Files
twenty/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts
Abdul Rahman c7139cbc84 feat: Add TS vector field filters support (#12376)
# Implementation Details
- Added support for 5 operators: `contains`, `containsAny`,
`containsAll`, `matches`, and `fuzzy`
- Works on any field of type `TS_VECTOR`
- Added PostgreSQL `pg_trgm` extension for fuzzy search functionality.
The extension provides the `similarity()` function needed for text
similarity searches.
- Not implemented in GraphQL

## Tradeoffs & Decisions
1. **Fuzzy Search Performance**: Using `pg_trgm` for fuzzy search is
more accurate but slower than simple text matching. We might want to add
a similarity threshold parameter in the future to control the tradeoff
between accuracy and performance.

2. **Operator Naming**: Chose `contains`/`containsAny`/`containsAll` to
be consistent with existing filter operators, though they might be less
intuitive than `search`/`searchAny`/`searchAll`.

## Demo


https://github.com/user-attachments/assets/790fc3ed-a188-4b49-864f-996a37481d99

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2025-05-30 15:54:50 +02:00

137 lines
4.0 KiB
TypeScript

import { ObjectLiteral } from 'typeorm';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
type WhereConditionParts = {
sql: string;
params: ObjectLiteral;
};
export const computeWhereConditionParts = ({
operator,
objectNameSingular,
key,
value,
}: {
operator: string;
objectNameSingular: string;
key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
}): WhereConditionParts => {
const uuid = Math.random().toString(36).slice(2, 7);
switch (operator) {
case 'isEmptyArray':
return {
sql: `"${objectNameSingular}"."${key}" = '{}'`,
params: {},
};
case 'eq':
return {
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
params: { [`${key}${uuid}`]: value },
};
case 'neq':
return {
sql: `"${objectNameSingular}"."${key}" != :${key}${uuid}`,
params: { [`${key}${uuid}`]: value },
};
case 'gt':
return {
sql: `"${objectNameSingular}"."${key}" > :${key}${uuid}`,
params: { [`${key}${uuid}`]: value },
};
case 'gte':
return {
sql: `"${objectNameSingular}"."${key}" >= :${key}${uuid}`,
params: { [`${key}${uuid}`]: value },
};
case 'lt':
return {
sql: `"${objectNameSingular}"."${key}" < :${key}${uuid}`,
params: { [`${key}${uuid}`]: value },
};
case 'lte':
return {
sql: `"${objectNameSingular}"."${key}" <= :${key}${uuid}`,
params: { [`${key}${uuid}`]: value },
};
case 'in':
return {
sql: `"${objectNameSingular}"."${key}" IN (:...${key}${uuid})`,
params: { [`${key}${uuid}`]: value },
};
case 'is':
return {
sql: `"${objectNameSingular}"."${key}" IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`,
params: {},
};
case 'like':
return {
sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` },
};
case 'ilike':
return {
sql: `"${objectNameSingular}"."${key}"::text ILIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` },
};
case 'startsWith':
return {
sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` },
};
case 'endsWith':
return {
sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` },
};
case 'contains':
return {
sql: `"${objectNameSingular}"."${key}" @> ARRAY[:...${key}${uuid}]`,
params: { [`${key}${uuid}`]: value },
};
case 'search': {
const tsQuery = value
.split(/\s+/)
.map((term: string) => `${term}:*`)
.join(' & ');
return {
sql: `(
"${objectNameSingular}"."${key}" @@ to_tsquery('simple', :${key}${uuid}Ts) OR
"${objectNameSingular}"."${key}"::text ILIKE :${key}${uuid}Like
)`,
params: {
[`${key}${uuid}Ts`]: tsQuery,
[`${key}${uuid}Like`]: `%${value}%`,
},
};
}
case 'notContains':
return {
sql: `NOT ("${objectNameSingular}"."${key}"::text[] && ARRAY[:...${key}${uuid}]::text[])`,
params: { [`${key}${uuid}`]: value },
};
case 'containsAny':
return {
sql: `"${objectNameSingular}"."${key}"::text[] && ARRAY[:...${key}${uuid}]::text[]`,
params: { [`${key}${uuid}`]: value },
};
case 'containsIlike':
return {
sql: `EXISTS (SELECT 1 FROM unnest("${objectNameSingular}"."${key}") AS elem WHERE elem ILIKE :${key}${uuid})`,
params: { [`${key}${uuid}`]: value },
};
default:
throw new GraphqlQueryRunnerException(
`Operator "${operator}" is not supported`,
GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
);
}
};