From c85a2d63f92faf58a23a57c7860e0fc18c9dbfb0 Mon Sep 17 00:00:00 2001 From: Olivier Costi Date: Fri, 17 Jul 2026 11:49:57 +0200 Subject: [PATCH 1/2] feat: add contains operator for filters --- .../static/queriesWithQueryLanguage.ts.txt | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt index eaa8da8..6df33e5 100644 --- a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt +++ b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt @@ -68,7 +68,7 @@ export type ComparisonOperator = export type LengthOperator = 'LENGTH'; -export type ArrayOperator = 'IN'; +export type ArrayOperator = 'IN' | 'CONTAINS'; export type NullOperator = 'NULL'; @@ -96,28 +96,44 @@ export type LengthExprWithModifier = export type FilterExpr = { [K in ComparisonOperator]?: T } & - { [K in ArrayOperator]?: T[] } & + { IN?: T[] } & { [K in keyof LengthExpr]?: never }; +export type ArrayFilterExpr = + FilterExpr & + { CONTAINS?: T | ReadonlyArray }; + export type FilterExprWithoutNull = RequireAtLeastOne> & { [K in NullOperator]?: never } & { [K in ModifierFunction]?: boolean }; +export type ArrayFilterExprWithoutNull = + RequireAtLeastOne> & + { [K in NullOperator]?: never } & + { [K in ModifierFunction]?: boolean }; + export type FilterExprWithNull = FilterExpr & { [K in NullOperator]?: boolean} & { [K in ModifierFunction]?: never }; +export type ArrayFilterExprWithNull = + ArrayFilterExpr & + { [K in NullOperator]?: boolean} & + { [K in ModifierFunction]?: never }; + export type MapOperators = LengthExprWithModifier | FilterExprWithoutNull | FilterExprWithNull; +export type ArrayMapOperators = LengthExprWithModifier | ArrayFilterExprWithoutNull | ArrayFilterExprWithNull; + export type SingleFilterExpr = { - [P in keyof T]?: T[P] extends Array | undefined - ? U extends Record - ? SingleFilterExpr | { NOT?: SingleFilterExpr } - : MapOperators - : T[P] extends Record | undefined - ? SingleFilterExpr | { NOT?: SingleFilterExpr } + [P in keyof T]?: NonNullable extends ReadonlyArray + ? NonNullable extends Record + ? SingleFilterExpr> | { NOT?: SingleFilterExpr> } + : ArrayMapOperators> + : NonNullable extends Record + ? SingleFilterExpr> | { NOT?: SingleFilterExpr> } : MapOperators; }; @@ -194,6 +210,7 @@ const comparisonOperatorMap: Record = { GE: '>=', LIKE: '~', IN: 'in', + CONTAINS: 'contains', NULL: 'null' }; @@ -280,6 +297,30 @@ const flattenWhere = ( )}]` ); } + } else if ((operator as Operator) === 'CONTAINS') { + const property = setModifiers.reduce( + (acc, [first]) => `${first.toLowerCase()}(${acc})`, + nestedPaths.some((path) => path === prop) ? nestedPaths.join('.') : [...nestedPaths, prop].join('.') + ); + const containsValues = Array.isArray(value) ? value : [value]; + const formatContainsValue = (containsValue: string | number) => + typeof containsValue === 'string' + ? setModifiers.reduce((acc, [first]) => `${first}(${acc})`, JSON.stringify(containsValue)) + : containsValue; + + if (containsValues.length === 0) { + entries.push('1 = 0'); + } else if (containsValues.length === 1) { + entries.push( + `${property} ${comparisonOperatorMap[operator as Operator]} ${formatContainsValue(containsValues[0])}` + ); + } else { + entries.push( + `(${containsValues + .map((containsValue) => `${property} ${comparisonOperatorMap[operator as Operator]} ${formatContainsValue(containsValue)}`) + .join(' or ')})` + ); + } } else if ((operator as LengthOperator) === 'LENGTH') { const lengthProp = `length(${setModifiers.reduce( (acc, [first]) => `${first.toLowerCase()}(${acc})`, From 6888145dcc1661071ba3e91d66f262addbecd007 Mon Sep 17 00:00:00 2001 From: Olivier Costi Date: Fri, 17 Jul 2026 11:52:01 +0200 Subject: [PATCH 2/2] chore: add documentation for contains operator --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 3dfa406..2a6421a 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,22 @@ wServices['article'].some({ "where" parameters are ANDed with other filter parameters. +Use `CONTAINS` for array properties like `string[]` or enum arrays. You can pass one value or multiple values. Multiple values are combined with `or`. + +```ts +wServices['article'].some({ + where: { + tags: { + CONTAINS: ['sale', 'new'] + } + } +}); +``` + +This evaluates to: + + tags contains "sale" or tags contains "new" + It is also possible to set an empty list within an IN-query: ```ts