chore: improved schema

This commit is contained in:
Morten Olsen
2025-12-10 20:45:02 +01:00
parent d02102977a
commit 0646390d52
16 changed files with 920 additions and 118 deletions

View File

@@ -1,6 +1,7 @@
import { createToken, Lexer, EmbeddedActionsParser } from 'chevrotain';
import type { ZodType } from 'zod';
import type { QueryFilter, QueryCondition } from './query-parser.schemas.js';
import { type QueryFilter, type QueryCondition, queryFilterSchema } from './query-parser.schemas.js';
// ----------------- Lexer -----------------
@@ -426,7 +427,10 @@ class QueryParserParser extends EmbeddedActionsParser {
return this.SUBRULE(this.#orExpression);
});
public parse = (input: string): QueryFilter => {
public parse = <T extends typeof queryFilterSchema>(
input: string,
schema: T = queryFilterSchema as unknown as T,
): QueryFilter => {
const lexResult = QueryLexer.tokenize(input);
if (lexResult.errors.length > 0) {
@@ -450,7 +454,7 @@ class QueryParserParser extends EmbeddedActionsParser {
throw new Error(`Parse error: ${error.message}`);
}
return result;
return schema.parse(result);
};
}

View File

@@ -1,85 +1,65 @@
import { z } from 'zod';
import { z, ZodArray } from 'zod';
const queryConditionTextSchema = z.object({
type: z.literal('text'),
tableName: z.string().optional(),
field: z.array(z.string()),
conditions: z.object({
equal: z.string().nullish(),
notEqual: z.string().optional(),
like: z.string().optional(),
notLike: z.string().optional(),
in: z.array(z.string()).optional(),
notIn: z.array(z.string()).optional(),
}),
});
const queryConditionTextSchema = z
.object({
type: z.literal('text'),
tableName: z.string().optional(),
field: z.array(z.string()),
conditions: z.object({
equal: z.string().nullish(),
notEqual: z.string().optional(),
like: z.string().optional(),
notLike: z.string().optional(),
in: z.array(z.string()).optional(),
notIn: z.array(z.string()).optional(),
}),
})
.meta({ id: 'QueryConditionText' });
type QueryConditionText = z.infer<typeof queryConditionTextSchema>;
const queryConditionNumberSchema = z.object({
type: z.literal('number'),
tableName: z.string().optional(),
field: z.array(z.string()),
conditions: z.object({
equals: z.number().nullish(),
notEquals: z.number().nullish(),
greaterThan: z.number().optional(),
greaterThanOrEqual: z.number().optional(),
lessThan: z.number().optional(),
lessThanOrEqual: z.number().optional(),
in: z.array(z.number()).optional(),
notIn: z.array(z.number()).optional(),
}),
});
const queryConditionNumberSchema = z
.object({
type: z.literal('number'),
tableName: z.string().optional(),
field: z.array(z.string()),
conditions: z.object({
equals: z.number().nullish(),
notEquals: z.number().nullish(),
greaterThan: z.number().optional(),
greaterThanOrEqual: z.number().optional(),
lessThan: z.number().optional(),
lessThanOrEqual: z.number().optional(),
in: z.array(z.number()).optional(),
notIn: z.array(z.number()).optional(),
}),
})
.meta({ id: 'QueryConditionNumber' });
type QueryConditionNumber = z.infer<typeof queryConditionNumberSchema>;
const queryConditionSchema = z.discriminatedUnion('type', [queryConditionTextSchema, queryConditionNumberSchema]);
const queryConditionSchema = z
.discriminatedUnion('type', [queryConditionTextSchema, queryConditionNumberSchema])
.meta({ id: 'QueryCondition' });
type QueryCondition = z.infer<typeof queryConditionSchema>;
type QueryFilter = QueryCondition | QueryOperator;
type QueryOperator = {
type: 'operator';
operator: 'and' | 'or';
conditions: QueryFilter[];
};
// Create a depth-limited recursive schema for OpenAPI compatibility
// This supports up to 3 levels of nesting, which should be sufficient for most use cases
// OpenAPI cannot handle z.lazy(), so we manually define the nesting
// If you need deeper nesting, you can add more levels (Level3, Level4, etc.)
const queryFilterSchemaLevel0: z.ZodType<QueryFilter> = z.union([
queryConditionSchema,
z.object({
const queryOperatorSchema = z
.object({
type: z.literal('operator'),
operator: z.enum(['and', 'or']),
conditions: z.array(queryConditionSchema),
}),
]);
get conditions(): ZodArray<typeof queryOperatorSchema | typeof queryConditionSchema> {
// eslint-disable-next-line
return z.array(queryFilterSchema) as any;
},
})
.meta({ id: 'QueryOperator' });
const queryFilterSchemaLevel1: z.ZodType<QueryFilter> = z.union([
queryConditionSchema,
z.object({
type: z.literal('operator'),
operator: z.enum(['and', 'or']),
conditions: z.array(queryFilterSchemaLevel0),
}),
]);
type QueryOperator = z.infer<typeof queryOperatorSchema>;
const queryFilterSchemaLevel2: z.ZodType<QueryFilter> = z.union([
queryConditionSchema,
z.object({
type: z.literal('operator'),
operator: z.enum(['and', 'or']),
conditions: z.array(queryFilterSchemaLevel1),
}),
]);
const queryFilterSchema = z.union([queryOperatorSchema, queryConditionSchema]).meta({ id: 'QueryFilter' });
// Export the depth-limited schema (supports 3 levels of nesting)
// This works with OpenAPI schema generation
const queryFilterSchema = queryFilterSchemaLevel2;
type QueryFilter = z.infer<typeof queryFilterSchema>;
export type { QueryConditionText, QueryConditionNumber, QueryOperator, QueryCondition, QueryFilter };
export { queryConditionSchema, queryFilterSchema };