Accept a singleton or filter in the GraphQL filter walker (#23738)
`RecordGqlOperationFilter` types `or` as `RecordGqlOperationFilter[] |
RecordGqlOperationFilter`, so both `{ or: [{ name: { ilike: '%acme%' }
}] }` and `{ or: { name: { ilike: '%acme%' } } }` are valid.
`applyLogicalGroup` went straight to `filters.forEach(...)`, so the
non-array form threw `TypeError: filters.forEach is not a function`
instead of returning records. Only `or` is affected: `and` is always an
array and `not` is always a single object.
This is not a new bug. The same assumption existed before the walker was
extracted, when `parseKeyFilter` did the `value.forEach` inline. Sentry
surfaced it on #23369, and it was left out of that PR to keep it scoped.
The fix mirrors `renderLogicalGroup` in the RLS SQL renderer, which
already normalizes a singleton to an array on its first line, so the two
walkers over this filter format now accept the same shapes.
This commit is contained in:
+51
-1
@@ -1,4 +1,7 @@
|
||||
import { createWhereExpressionRecorder } from 'test/utils/create-where-expression-recorder.util';
|
||||
import {
|
||||
createWhereExpressionRecorder,
|
||||
type RecordedWhereCall,
|
||||
} from 'test/utils/create-where-expression-recorder.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
@@ -74,6 +77,21 @@ const recordFilterEntries = (filter: Record<string, unknown>) => {
|
||||
return recorder.calls;
|
||||
};
|
||||
|
||||
const withNormalizedParameterKeys = (calls: RecordedWhereCall[]): unknown[] =>
|
||||
calls.map(({ method, node }) =>
|
||||
node.kind === 'sql'
|
||||
? {
|
||||
method,
|
||||
sql: node.sql.replace(/(?<!:):[A-Za-z0-9_]+/g, ':parameter'),
|
||||
parameterValues: Object.values(node.parameters ?? {}),
|
||||
}
|
||||
: {
|
||||
method,
|
||||
kind: node.kind,
|
||||
children: withNormalizedParameterKeys(node.children),
|
||||
},
|
||||
);
|
||||
|
||||
describe('GraphqlQueryFilterConditionParser', () => {
|
||||
describe('applyFilterEntriesToWhereBrackets', () => {
|
||||
it('emits the first entry with where and later entries with andWhere', () => {
|
||||
@@ -170,6 +188,38 @@ describe('GraphqlQueryFilterConditionParser', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies an or group that is not wrapped in an array', () => {
|
||||
const calls = recordFilterEntries({ or: { name: { ilike: '%acme%' } } });
|
||||
|
||||
expect(withNormalizedParameterKeys(calls)).toEqual(
|
||||
withNormalizedParameterKeys(
|
||||
recordFilterEntries({ or: [{ name: { ilike: '%acme%' } }] }),
|
||||
),
|
||||
);
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
method: 'where',
|
||||
node: {
|
||||
kind: 'brackets',
|
||||
children: [
|
||||
{
|
||||
method: 'where',
|
||||
node: {
|
||||
kind: 'brackets',
|
||||
children: [
|
||||
{
|
||||
method: 'where',
|
||||
node: expect.objectContaining({ kind: 'sql' }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits a not group as notBrackets', () => {
|
||||
const calls = recordFilterEntries({ not: { name: { ilike: '%a%' } } });
|
||||
|
||||
|
||||
+4
-2
@@ -94,13 +94,15 @@ const applyFilterEntry = (
|
||||
|
||||
const applyLogicalGroup = (
|
||||
whereExpression: WhereExpressionBuilder,
|
||||
filters: Record<string, unknown>[],
|
||||
filters: Record<string, unknown>[] | Record<string, unknown>,
|
||||
logicalOperator: 'and' | 'or',
|
||||
isFirst: boolean,
|
||||
context: FilterWalkContext,
|
||||
): void => {
|
||||
const filterList = Array.isArray(filters) ? filters : [filters];
|
||||
|
||||
const groupCondition = new Brackets((groupWhereExpression) => {
|
||||
filters.forEach((filter, index) => {
|
||||
filterList.forEach((filter, index) => {
|
||||
const elementCondition = new Brackets((elementWhereExpression) => {
|
||||
applyFilterEntries(elementWhereExpression, filter, context);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user