Add wildcard documentation for like/ilike/containsIlike filters (#17825)
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%). <img width="651" height="409" alt="image" src="https://github.com/user-attachments/assets/b3537af6-a0b0-4fff-a86d-a9ae334d628e" /> 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.) <img width="878" height="458" alt="image" src="https://github.com/user-attachments/assets/ab0f4e7c-df50-45ef-b1c8-e43c8881a9a3" /><img width="482" height="288" alt="image" src="https://github.com/user-attachments/assets/20dc39ee-2417-4ecc-810e-ea0ead33d803" /> --------- Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
+2
-2
@@ -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':
|
||||
|
||||
+4
-1
@@ -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 },
|
||||
},
|
||||
|
||||
+4
-1
@@ -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%)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+4
-1
@@ -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%)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+9
-2
@@ -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 },
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export enum FilterComparators {
|
||||
lt = 'lt',
|
||||
lte = 'lte',
|
||||
startsWith = 'startsWith',
|
||||
endsWith = 'endsWith',
|
||||
like = 'like',
|
||||
ilike = 'ilike',
|
||||
|
||||
|
||||
+6
-1
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+3
@@ -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',
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user