update
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import type {
|
||||
QueryCondition,
|
||||
QueryConditionNumber,
|
||||
QueryConditionText,
|
||||
QueryFilter,
|
||||
} from '@morten-olsen/stash-query-dsl';
|
||||
import { type Knex } from 'knex';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Escapes a JSON key for use in PostgreSQL JSON operators.
|
||||
* Escapes single quotes by doubling them, which is the PostgreSQL standard.
|
||||
@@ -30,74 +34,6 @@ const getFieldSelector = (query: Knex.QueryBuilder, field: string[], tableName?:
|
||||
return query.client.raw(sqlExpression);
|
||||
};
|
||||
|
||||
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({
|
||||
example: {
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Equal condition',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Like condition',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['content'],
|
||||
conditions: {
|
||||
like: '%cat%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'In condition',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
in: ['demo', 'article', 'post'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Null check',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['source'],
|
||||
conditions: {
|
||||
equal: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type QueryConditionText = z.infer<typeof queryConditionTextSchema>;
|
||||
|
||||
const applyQueryConditionText = (query: Knex.QueryBuilder, { field, tableName, conditions }: QueryConditionText) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equal) {
|
||||
@@ -127,77 +63,6 @@ const applyQueryConditionText = (query: Knex.QueryBuilder, { field, tableName, c
|
||||
return query;
|
||||
};
|
||||
|
||||
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({
|
||||
example: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
equals: 1,
|
||||
},
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Equals condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
equals: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Greater than condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
greaterThan: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Range condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
greaterThanOrEqual: 1,
|
||||
lessThanOrEqual: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'In condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
in: [1, 2, 3],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type QueryConditionNumber = z.infer<typeof queryConditionNumberSchema>;
|
||||
|
||||
const applyQueryConditionNumber = (
|
||||
query: Knex.QueryBuilder,
|
||||
{ field, tableName, conditions }: QueryConditionNumber,
|
||||
@@ -236,10 +101,6 @@ const applyQueryConditionNumber = (
|
||||
return query;
|
||||
};
|
||||
|
||||
const queryConditionSchema = z.discriminatedUnion('type', [queryConditionTextSchema, queryConditionNumberSchema]);
|
||||
|
||||
type QueryCondition = z.infer<typeof queryConditionSchema>;
|
||||
|
||||
const applyQueryCondition = (query: Knex.QueryBuilder, options: QueryCondition) => {
|
||||
switch (options.type) {
|
||||
case 'text': {
|
||||
@@ -254,254 +115,6 @@ const applyQueryCondition = (query: Knex.QueryBuilder, options: QueryCondition)
|
||||
}
|
||||
};
|
||||
|
||||
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({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
conditions: z.array(queryConditionSchema),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'AND operator',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'OR operator',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const queryFilterSchemaLevel1: z.ZodType<QueryFilter> = z.union([
|
||||
queryConditionSchema,
|
||||
z
|
||||
.object({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
conditions: z.array(queryFilterSchemaLevel0),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Nested AND within OR',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const queryFilterSchemaLevel2: z.ZodType<QueryFilter> = z.union([
|
||||
queryConditionSchema,
|
||||
z
|
||||
.object({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
conditions: z.array(queryFilterSchemaLevel1),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Complex nested query',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
// Export the depth-limited schema (supports 3 levels of nesting)
|
||||
// This works with OpenAPI schema generation
|
||||
const queryFilterSchema = queryFilterSchemaLevel2;
|
||||
|
||||
const applyQueryFilter = (query: Knex.QueryBuilder, filter: QueryFilter) => {
|
||||
if (filter.type === 'operator') {
|
||||
if (filter.conditions.length === 0) {
|
||||
@@ -545,5 +158,4 @@ const applyQueryFilter = (query: Knex.QueryBuilder, filter: QueryFilter) => {
|
||||
}
|
||||
};
|
||||
|
||||
export type { QueryConditionText, QueryConditionNumber, QueryOperator, QueryCondition, QueryFilter };
|
||||
export { applyQueryCondition, queryConditionSchema, queryFilterSchema, applyQueryFilter };
|
||||
export { applyQueryCondition, applyQueryFilter };
|
||||
|
||||
Reference in New Issue
Block a user