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