From 52e57e70fdcdde3e9fa160da22f4d51c3ee14359 Mon Sep 17 00:00:00 2001 From: BugIsGod <87571967+bugisthegod@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:32:41 +0000 Subject: [PATCH] Add wildcard documentation for like/ilike/containsIlike filters (#17825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add documentation for issue #16602 After discussing with the team (Thomas), https://discord.com/channels/1130383047699738754/1443986309436936212 we decided that updating the documentation. The issue is In compute-where-condition-parts.ts, the like/ilike cases pass values directly to SQL without adding % wildcards for api using, so they behave like exact matches. This PR updates the documentation regarding the use of `like`, `ilike` and `containsIlike` filters. Instead of auto-wrapping values with % wildcards in the backend, we are choosing to leave the control to the API users (%value% or value%). image But I add wildcard for `startsWith` and `endsWith` because these operators have a fixed semantic meaning. (To see the results, please refresh the cache first, then restart the server.) imageimage --------- Co-authored-by: Thomas Trompette --- .../utils/compute-where-condition-parts.ts | 4 ++-- .../graphql-types/input/array-filter.input-type.ts | 5 ++++- .../graphql-types/input/raw-json-filter.input-type.ts | 5 ++++- .../graphql-types/input/rich-text.input-type.ts | 5 ++++- .../graphql-types/input/string-filter.input-type.ts | 11 +++++++++-- .../filter-parser-utils/parse-base-filter.util.ts | 1 + .../open-api/utils/__tests__/parameters.utils.spec.ts | 7 ++++++- .../core-modules/open-api/utils/base-schema.utils.ts | 3 ++- .../core-modules/open-api/utils/parameters.utils.ts | 7 ++++++- .../zod-schemas/field-filters.zod-schema.ts | 3 +++ 10 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts index 8fb4d9af4b1..d20bf040fd9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts @@ -110,12 +110,12 @@ export const computeWhereConditionParts = ({ }; case 'startsWith': return { - sql: `${fieldReference}::text LIKE :${key}${uuid}`, + sql: `${fieldReference}::text ^@ :${key}${uuid}`, params: { [`${key}${uuid}`]: `${value}` }, }; case 'endsWith': return { - sql: `${fieldReference}::text LIKE :${key}${uuid}`, + sql: `RIGHT(${fieldReference}::text, LENGTH(:${key}${uuid})) = :${key}${uuid}`, params: { [`${key}${uuid}`]: `${value}` }, }; case 'contains': diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts index 118eb898fc7..ce35529da91 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts @@ -5,7 +5,10 @@ import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphq export const ArrayFilterType = new GraphQLInputObjectType({ name: 'ArrayFilter', fields: { - containsIlike: { type: GraphQLString }, + containsIlike: { + type: GraphQLString, + description: 'Case-insensitive match with % wildcard (e.g. %value%)', + }, is: { type: FilterIs }, isEmptyArray: { type: GraphQLBoolean }, }, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts index 75f40b7e591..6be4b09e66f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts @@ -6,6 +6,9 @@ export const RawJsonFilterType = new GraphQLInputObjectType({ name: 'RawJsonFilter', fields: { is: { type: FilterIs }, - like: { type: GraphQLString }, + like: { + type: GraphQLString, + description: 'Pattern match with % wildcard (e.g. %value%)', + }, }, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts index e99353b1698..847a2e31ff9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts @@ -3,7 +3,10 @@ import { GraphQLInputObjectType, GraphQLString } from 'graphql'; const richTextV2LeafFilter = new GraphQLInputObjectType({ name: 'RichTextV2LeafFilter', fields: { - ilike: { type: GraphQLString }, + ilike: { + type: GraphQLString, + description: 'Case-insensitive match with % wildcard (e.g. %value%)', + }, }, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts index 3ea34f85b0d..52142126eaa 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/string-filter.input-type.ts @@ -18,8 +18,15 @@ export const StringFilterType = new GraphQLInputObjectType({ lte: { type: GraphQLString }, neq: { type: GraphQLString }, startsWith: { type: GraphQLString }, - like: { type: GraphQLString }, - ilike: { type: GraphQLString }, + endsWith: { type: GraphQLString }, + like: { + type: GraphQLString, + description: 'Pattern match with % wildcard (e.g. %value%)', + }, + ilike: { + type: GraphQLString, + description: 'Case-insensitive match with % wildcard (e.g. %value%)', + }, regex: { type: GraphQLString }, iregex: { type: GraphQLString }, is: { type: FilterIs }, diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util.ts index 0381c45bf78..077def6113b 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util.ts @@ -11,6 +11,7 @@ export enum FilterComparators { lt = 'lt', lte = 'lte', startsWith = 'startsWith', + endsWith = 'endsWith', like = 'like', ilike = 'ilike', diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts index 735f9427629..87c791c8d28 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts @@ -77,7 +77,8 @@ describe('computeParameters', () => { expect(computeFilterParameters()).toEqual({ name: 'filter', in: 'query', - description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2 + description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2. + For like/ilike, use % as a wildcard (e.g. %value% for substring match). Refer to the filter section at the top of the page for more details.`, required: false, schema: { @@ -97,6 +98,10 @@ describe('computeParameters', () => { 'or(createdAt[gte]:"2024-01-01",createdAt[lte]:"2023-01-01",not(id[is]:NULL))', description: 'A more complex filter param', }, + like: { + value: 'name[like]:"%value%"', + description: 'Pattern matching', + }, }, }); }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts index ad3c11cf532..5d622bd2ee6 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts @@ -13,7 +13,7 @@ export const baseSchema = ( openapi: '3.1.1', info: { title: 'Twenty Api', - description: `Use this page to explore and call the **REST API**. + description: `Use this page to explore and call the **REST API**. ## Authentication @@ -40,6 +40,7 @@ Use the \`filter\` query parameter to narrow results. - Multiple conditions: \`field1[eq]:1,field2[gte]:10\` (root conjunction is AND) - Composite fields: \`field.subField[COMPARATOR]:value\` - Common comparators: \`eq\`, \`neq\`, \`in\`, \`containsAny\`, \`is\`, \`gt\`, \`gte\`, \`lt\`, \`lte\`, \`startsWith\`, \`like\`, \`ilike\` +- Wildcards: For \`like\`/\`ilike\`, use \`%\` as a wildcard (e.g. \`%value%\` for substring match) Examples: diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index b289bc75bf8..b4bbeb6a95e 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -93,7 +93,8 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { return { name: 'filter', in: 'query', - description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2 + description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2. + For like/ilike, use % as a wildcard (e.g. %value% for substring match). Refer to the filter section at the top of the page for more details.`, required: false, schema: { @@ -113,6 +114,10 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { 'or(createdAt[gte]:"2024-01-01",createdAt[lte]:"2023-01-01",not(id[is]:NULL))', description: 'A more complex filter param', }, + like: { + value: 'name[like]:"%value%"', + description: 'Pattern matching', + }, }, }; }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts index a81c390f99e..ac6709b9fd8 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts @@ -44,6 +44,7 @@ export const generateFieldFilterZodSchema = ( .optional() .describe('Case-insensitive pattern match (use % for wildcards)'), startsWith: z.string().optional().describe('Starts with'), + endsWith: z.string().optional().describe('Ends with'), is: NullCheckEnum.optional().describe('Is null or not null'), }) .optional() @@ -256,6 +257,7 @@ export const generateFieldFilterZodSchema = ( .string() .optional() .describe('First name starts with'), + endsWith: z.string().optional().describe('First name ends with'), is: NullCheckEnum.optional().describe( 'First name is null or not null', ), @@ -278,6 +280,7 @@ export const generateFieldFilterZodSchema = ( .string() .optional() .describe('Last name starts with'), + endsWith: z.string().optional().describe('Last name ends with'), is: NullCheckEnum.optional().describe( 'Last name is null or not null', ),