feat: improved zod handling
Some checks failed
Build and release / Build (push) Failing after 2m41s
Build and release / update-release-draft (push) Has been skipped
Build and release / Release (push) Has been skipped

This commit is contained in:
Morten Olsen
2025-12-11 08:36:40 +01:00
parent 3c475ab5d6
commit 68abe3ce79
10 changed files with 66 additions and 39 deletions

View File

@@ -1,3 +1,4 @@
export * from './query-parser.schemas.js'; export * from './query-parser.schemas.js';
export { QueryParser } from './query-parser.js'; export { QueryParser, queryParser } from './query-parser.js';
export * from './utils.filter.js'; export * from './utils.filter.js';
export * from './query-parser.codec.js';

View File

@@ -0,0 +1,20 @@
import { z } from 'zod';
import { queryFilterSchema } from './query-parser.schemas.js';
import { queryParser } from './query-parser.js';
const queryStringSchema: typeof queryFilterSchema = z
.codec(z.string(), queryFilterSchema, {
encode: (filter) => {
return queryParser.stringify(filter);
},
decode: (input) => {
return queryParser.parse(input);
},
// eslint-disable-next-line
}).meta({ id: 'QueryString', examples: ["metadata.foo = 'bar'"] }) as any;
// eslint-disable-next-line
const querySchema: typeof queryFilterSchema = z.union([queryStringSchema, queryFilterSchema]) as any
export { querySchema };

View File

@@ -19,4 +19,6 @@ class QueryParser {
}; };
} }
export { QueryParser }; const queryParser = new QueryParser();
export { QueryParser, queryParser };

View File

@@ -1,13 +1,11 @@
import type { TableRows } from '../database/database.js'; import type { TableRows } from '../database/database.js';
import type { DocumentChunk } from './document-chunks.schemas.js'; import { documentChunkWithDistanceSchema } from './document-chunks.schemas.js';
const mapFromDocumentChunkRow = ( const mapFromDocumentChunkRow = (
row: TableRows['documentChunks'] & { row: TableRows['documentChunks'] & {
metadata: unknown; metadata?: unknown;
}, },
): DocumentChunk => ({ ) => documentChunkWithDistanceSchema.parse(row);
...row,
});
export { mapFromDocumentChunkRow }; export { mapFromDocumentChunkRow };

View File

@@ -1,7 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { queryFilterSchema } from '@morten-olsen/stash-query-dsl'; import { querySchema } from '@morten-olsen/stash-query-dsl';
import { createListResultSchema, queryDSLSchema } from '../../utils/utils.schema.js'; import { createListResultSchema } from '../../utils/utils.schema.js';
const documentChunkSchema = z const documentChunkSchema = z
.object({ .object({
@@ -19,21 +19,28 @@ const documentChunkFilterSchema = z
limit: z.number().default(20), limit: z.number().default(20),
offset: z.number().default(0), offset: z.number().default(0),
semanticText: z.string().optional(), semanticText: z.string().optional(),
conditions: z.union([queryDSLSchema, queryFilterSchema]).optional(), conditions: querySchema.optional(),
}) })
.meta({ id: 'DocumentChunkFilter' }); .meta({ id: 'DocumentChunkFilter' });
type DocumentChunkFilter = z.infer<typeof documentChunkFilterSchema>; type DocumentChunkFilter = z.infer<typeof documentChunkFilterSchema>;
const documentChunksFindResultSchema = createListResultSchema( const documentChunkWithDistanceSchema = documentChunkSchema
documentChunkSchema
.extend({ .extend({
distance: z.number().optional(), distance: z.number().optional(),
}) })
.meta({ id: 'DocumentChunkWithDistance' }), .meta({ id: 'DocumentChunkWithDistance' });
).meta({ id: 'DocumentChunkFindResult' });
const documentChunksFindResultSchema = createListResultSchema(documentChunkWithDistanceSchema).meta({
id: 'DocumentChunkFindResult',
});
type DocumentChunksFindResult = z.infer<typeof documentChunksFindResultSchema>; type DocumentChunksFindResult = z.infer<typeof documentChunksFindResultSchema>;
export type { DocumentChunk, DocumentChunkFilter, DocumentChunksFindResult }; export type { DocumentChunk, DocumentChunkFilter, DocumentChunksFindResult };
export { documentChunkSchema, documentChunkFilterSchema, documentChunksFindResultSchema }; export {
documentChunkSchema,
documentChunkFilterSchema,
documentChunksFindResultSchema,
documentChunkWithDistanceSchema,
};

View File

@@ -1,5 +1,3 @@
import { QueryParser } from '@morten-olsen/stash-query-dsl';
import { DatabaseService, tableNames, type TableRows } from '../database/database.js'; import { DatabaseService, tableNames, type TableRows } from '../database/database.js';
import { EmbeddingsService } from '../embeddings/embeddings.js'; import { EmbeddingsService } from '../embeddings/embeddings.js';
import type { Services } from '../../utils/utils.services.js'; import type { Services } from '../../utils/utils.services.js';
@@ -44,11 +42,7 @@ class DocumentChunksService {
query = query.orderBy('createdAt', 'desc'); query = query.orderBy('createdAt', 'desc');
} }
if (filter.conditions) { if (filter.conditions) {
const parser = this.#services.get(QueryParser); query = applyQueryFilter(query, filter.conditions);
query = applyQueryFilter(
query,
typeof filter.conditions === 'string' ? parser.parse(filter.conditions) : filter.conditions,
);
} }
query = query.limit(filter.limit).offset(filter.offset); query = query.limit(filter.limit).offset(filter.offset);
@@ -56,7 +50,7 @@ class DocumentChunksService {
const items = await query; const items = await query;
return { return {
items: items.map(mapFromDocumentChunkRow as ExplicitAny), items: items.map(mapFromDocumentChunkRow),
}; };
}; };
} }

View File

@@ -1,12 +1,7 @@
import type { TableRows } from '../database/database.js'; import type { TableRows } from '../database/database.js';
import type { Document } from './documents.schemas.js'; import { documentSchema, type Document } from './documents.schemas.js';
const mapFromDocumentRow = (row: TableRows['documents']): Document => ({ const mapFromDocumentRow = (row: TableRows['documents']): Document => documentSchema.parse(row);
...row,
createdAt: row.createdAt.toISOString?.(),
updatedAt: row.updatedAt.toISOString?.(),
deletedAt: row.deletedAt?.toISOString?.() || null,
});
export { mapFromDocumentRow }; export { mapFromDocumentRow };

View File

@@ -6,10 +6,10 @@ import { createListResultSchema, queryDSLSchema } from '../../utils/utils.schema
const documentSchema = z const documentSchema = z
.object({ .object({
id: z.guid(), id: z.guid(),
owner: z.string().nullable(), owner: z.guid().nullable(),
createdAt: z.iso.datetime(), createdAt: z.coerce.date(),
updatedAt: z.iso.datetime(), updatedAt: z.coerce.date(),
deletedAt: z.iso.datetime().nullable(), deletedAt: z.coerce.date().nullable(),
contentType: z.string().nullable(), contentType: z.string().nullable(),
text: z.string().nullable(), text: z.string().nullable(),
source: z.string().nullable(), source: z.string().nullable(),

View File

@@ -1,5 +1,8 @@
import { QueryParser } from '@morten-olsen/stash-query-dsl';
import { z, type ZodType } from 'zod'; import { z, type ZodType } from 'zod';
const parser = new QueryParser();
const createListResultSchema = <T extends ZodType>(schema: T) => const createListResultSchema = <T extends ZodType>(schema: T) =>
z.object({ z.object({
items: z.array(schema), items: z.array(schema),
@@ -8,8 +11,15 @@ const createListResultSchema = <T extends ZodType>(schema: T) =>
const queryDSLSchema = z const queryDSLSchema = z
.string() .string()
.describe('Query DSL based filter') .describe('Query DSL based filter')
.superRefine((value, context) => {
try {
parser.parse(value);
} catch (err) {
context.addIssue(String(err));
}
})
.meta({ .meta({
id: 'QueryDSLString', id: 'QueryDQLString',
examples: ["metadata.foo = 'bar'"], examples: ["metadata.foo = 'bar'"],
}); });

View File

@@ -31,7 +31,7 @@ const createApi = async (runtime: StashRuntime = new StashRuntime()) => {
runtime.documents.subscribe({ runtime.documents.subscribe({
filter: "metadata.foo = 'bar'", filter: "metadata.foo = 'bar'",
fn: (document) => { fn: (document) => {
console.log(document); // console.log(document);
}, },
}); });
const app = fastify().withTypeProvider<ZodTypeProvider>(); const app = fastify().withTypeProvider<ZodTypeProvider>();