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%).
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.)

---------
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',
),