From 4f707bb9f157e7b89b581fbdbaaeba47f9d1decc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 15 May 2026 14:04:46 +0200 Subject: [PATCH] refactor(twenty-shared): split turnRecordFilterIntoGqlOperationFilter into dispatcher + direct builder (#20600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on top of #20533 — merge that one first. ## Summary Splits `turnRecordFilterIntoRecordGqlOperationFilter` into a thin dispatcher and a private `buildDirectFieldGqlOperationFilter`. Same observable behaviour, but: - The two ordering-constraint comments that #20533 introduced go away (\"must run before the emptiness shortcut\", \"drop rather than fall through to legacy relation-by-record\"). They were both flagging a real code smell: dispatch and direct-filter logic were interleaved in the same function body and depended on a flag inside the input. - Removes the self-recursion + \"inject target into fieldMetadataItems\" hack. - Removes three duplicated `fieldMetadataItems.find(...)` lookups that only existed to compose error-message labels — the resolved `fieldMetadataItem` was already in scope. - Renames `correspondingFieldMetadataItem` → `fieldMetadataItem` inside the extracted function (the variable IS the field; the longer prefix predated the split). ## Why Discussed in #20533: the conditional ordering between the relation-traversal branch and the per-type switch was easy to get wrong, and the legacy `case 'RELATION':` foot-gun (parsing the filter value as a UUID list when it's actually a text value) was guarded by a defensive `return` rather than by structure. After this PR the dispatcher picks exactly one branch up front and `buildDirectFieldGqlOperationFilter` only ever runs against the field the filter operates on — no \"is this the source field or the target field?\" ambiguity inside the switch. ## Test plan - [x] `npx nx build twenty-shared` succeeds (no type regressions). - [x] `jest packages/twenty-shared` — 1212 tests pass. - [x] `jest view-query-params.service.spec.ts` — 7 tests pass, including the relation-traversal round-trip case. - [ ] CI green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ...RecordFilterIntoGqlOperationFilter.test.ts | 88 ++++++ .../turnRecordFilterIntoGqlOperationFilter.ts | 273 +++++++++--------- 2 files changed, 225 insertions(+), 136 deletions(-) diff --git a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts index 60f46595cb2..c1ca70f0e3e 100644 --- a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts @@ -919,4 +919,92 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { expect(result).toHaveProperty('recordId.in'); }); }); + + describe('relation traversal', () => { + // The dispatcher should wrap the inner filter under the relation source + // field's GraphQL key and build it against the target field's type + // (not the relation FK's type). + it('should nest filter under source field name when target field is set', () => { + const result = turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: { + ...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'), + relationTargetFieldMetadataId: 'f-text', + } as RecordFilter, + fieldMetadataItems: fields, + }); + + expect(result).toEqual({ company: { name: { ilike: '%Acme%' } } }); + }); + + // If the target field is no longer in fieldMetadataItems (e.g. it was + // deleted from the workspace), dropping the filter is the safe path — + // the alternative would silently interpret the text value as a UUID + // list against the relation FK. + it('should return undefined when target field is not found', () => { + const result = turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: { + ...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'), + relationTargetFieldMetadataId: 'nonexistent-target', + } as RecordFilter, + fieldMetadataItems: fields, + }); + + expect(result).toBeUndefined(); + }); + + // Emptiness operands must check the target column on the related + // object rather than the FK column on the source record. + it('should apply IS_EMPTY against the target field, not the relation FK', () => { + const result = turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: { + ...makeFilter('f-relation', RecordFilterOperand.IS_EMPTY, ''), + relationTargetFieldMetadataId: 'f-text', + } as RecordFilter, + fieldMetadataItems: fields, + }); + + // Without traversal, the RELATION case would have produced a filter + // on `companyId`; here we expect the emptiness check to fall on + // `company.name` instead. + expect(result).toEqual({ + company: { or: [{ name: { ilike: '' } }, { name: { is: 'NULL' } }] }, + }); + }); + + // The target field's per-type switch must run, so number/select/etc. + // targets behave like a direct filter on that field — verified here + // with a SELECT target. + it('should dispatch to target type switch (SELECT target)', () => { + const result = turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: { + ...makeFilter('f-relation', RecordFilterOperand.IS, '["ACTIVE"]'), + relationTargetFieldMetadataId: 'f-select', + } as RecordFilter, + fieldMetadataItems: fields, + }); + + expect(result).toEqual({ company: { status: { in: ['ACTIVE'] } } }); + }); + + // Without a relationTargetFieldMetadataId the dispatcher must fall + // through to the direct builder — preserving the filter-by-record-id + // behaviour on the relation FK. + it('should keep relation filter-by-id behaviour when no target field is set', () => { + const result = turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies, + recordFilter: makeFilter( + 'f-relation', + RecordFilterOperand.IS, + '["550e8400-e29b-41d4-a716-446655440000"]', + ), + fieldMetadataItems: fields, + }); + + expect(result).toHaveProperty('companyId.in'); + }); + }); }); diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index aa607ce277d..1889bcb0a86 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -75,11 +75,11 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }: TurnRecordFilterIntoRecordGqlOperationFilterParams): | RecordGqlOperationFilter | undefined => { - const correspondingFieldMetadataItem = fieldMetadataItems.find( + const sourceFieldMetadataItem = fieldMetadataItems.find( (field) => field.id === recordFilter.fieldMetadataId, ); - if (!isDefined(correspondingFieldMetadataItem)) { + if (!isDefined(sourceFieldMetadataItem)) { return; } @@ -87,13 +87,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return; } - // Must run before the emptiness shortcut so an "is empty" filter on - // `company.name` is evaluated against `Company.name`, not the FK column. - // If the target field can't be resolved the filter is dropped rather than - // fed to the per-type switch as a RELATION (which would parse `value` as - // a UUID list and silently mishandle target-field values like "Acme"). if ( - correspondingFieldMetadataItem.type === FieldMetadataType.RELATION && + sourceFieldMetadataItem.type === FieldMetadataType.RELATION && isDefined(recordFilter.relationTargetFieldMetadataId) ) { const targetFieldMetadataItem = fieldMetadataItems.find( @@ -104,13 +99,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return; } - const innerFilter = turnRecordFilterIntoRecordGqlOperationFilter({ + const innerFilter = buildDirectFieldGqlOperationFilter({ recordFilter: { ...recordFilter, fieldMetadataId: targetFieldMetadataItem.id, relationTargetFieldMetadataId: null, }, - fieldMetadataItems, + fieldMetadataItem: targetFieldMetadataItem, filterValueDependencies, }); @@ -119,19 +114,39 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } return { - [correspondingFieldMetadataItem.name]: innerFilter, + [sourceFieldMetadataItem.name]: innerFilter, } as RecordGqlOperationFilter; } + return buildDirectFieldGqlOperationFilter({ + recordFilter, + fieldMetadataItem: sourceFieldMetadataItem, + filterValueDependencies, + }); +}; + +type BuildDirectFieldGqlOperationFilterParams = { + filterValueDependencies: RecordFilterValueDependencies; + recordFilter: Omit; + fieldMetadataItem: FieldShared; +}; + +const buildDirectFieldGqlOperationFilter = ({ + recordFilter, + fieldMetadataItem, + filterValueDependencies, +}: BuildDirectFieldGqlOperationFilterParams): + | RecordGqlOperationFilter + | undefined => { const shouldComputeEmptinessFilter = checkIfShouldComputeEmptinessFilter({ recordFilterOperand: recordFilter.operand, - correspondingFieldMetadataItem, + correspondingFieldMetadataItem: fieldMetadataItem, }); if (shouldComputeEmptinessFilter) { const emptinessFilter = getEmptyRecordGqlOperationFilter({ operand: recordFilter.operand, - correspondingField: correspondingFieldMetadataItem, + correspondingField: fieldMetadataItem, recordFilter: recordFilter, }); @@ -142,23 +157,21 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ const isSubFieldFilter = isNonEmptyString(subFieldName); - const filterType = getFilterTypeFromFieldType( - correspondingFieldMetadataItem.type, - ); + const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); switch (filterType) { case 'TEXT': switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { ilike: `%${recordFilter.value}%`, } as StringFilter, }; case RecordFilterOperand.DOES_NOT_CONTAIN: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { ilike: `%${recordFilter.value}%`, } as StringFilter, }, @@ -173,7 +186,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.VECTOR_SEARCH: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { search: recordFilter.value, } as TSVectorFilter, }; @@ -186,14 +199,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { like: `%${recordFilter.value}%`, } as RawJsonFilter, }; case RecordFilterOperand.DOES_NOT_CONTAIN: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { like: `%${recordFilter.value}%`, } as RawJsonFilter, }, @@ -207,14 +220,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { like: `%${recordFilter.value}%`, } as FilesFilter, }; case RecordFilterOperand.DOES_NOT_CONTAIN: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { like: `%${recordFilter.value}%`, } as FilesFilter, }, @@ -251,12 +264,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { and: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: start, } as DateFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: end, } as DateFilter, }, @@ -277,19 +290,19 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS_IN_PAST: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: nowAsPlainDate, } as DateFilter, }; case RecordFilterOperand.IS_IN_FUTURE: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: nowAsPlainDate, } as DateFilter, }; case RecordFilterOperand.IS_TODAY: { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { eq: nowAsPlainDate, } as DateFilter, }; @@ -301,14 +314,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS_AFTER: { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: plainDateFilter, } as DateFilter, }; } case RecordFilterOperand.IS_BEFORE: { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: plainDateFilter, } as DateFilter, }; @@ -316,7 +329,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ case RecordFilterOperand.IS: { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { eq: plainDateFilter, } as DateFilter, }; @@ -367,12 +380,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { and: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: start.toInstant().toString(), } as DateTimeFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: end.toInstant().toString(), } as DateTimeFilter, }, @@ -393,13 +406,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS_IN_PAST: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: now.toInstant().round('minute').toString(), } as DateTimeFilter, }; case RecordFilterOperand.IS_IN_FUTURE: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gt: now.toInstant().round('minute').toString(), } as DateTimeFilter, }; @@ -407,12 +420,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { and: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: getPeriodStart(now, 'DAY').toInstant().toString(), } as DateTimeFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: getNextPeriodStart(now, 'DAY').toInstant().toString(), } as DateTimeFilter, }, @@ -449,12 +462,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { and: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: start.toString(), } as DateTimeFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: end.toString(), } as DateTimeFilter, }, @@ -467,14 +480,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS_AFTER: { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: resolvedDateTime.toString(), } as DateTimeFilter, }; } case RecordFilterOperand.IS_BEFORE: { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lt: resolvedDateTime.toString(), } as DateTimeFilter, }; @@ -490,13 +503,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { eq: convertRatingToRatingValue(parseFloat(recordFilter.value)), } as RatingFilter, }; case RecordFilterOperand.GREATER_THAN_OR_EQUAL: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { in: convertGreaterThanOrEqualRatingToArrayOfRatingValues( parseFloat(recordFilter.value), ), @@ -504,7 +517,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }; case RecordFilterOperand.LESS_THAN_OR_EQUAL: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { in: convertLessThanOrEqualRatingToArrayOfRatingValues( parseFloat(recordFilter.value), ), @@ -519,26 +532,26 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.GREATER_THAN_OR_EQUAL: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { gte: parseFloat(recordFilter.value), } as FloatFilter, }; case RecordFilterOperand.LESS_THAN_OR_EQUAL: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { lte: parseFloat(recordFilter.value), } as FloatFilter, }; case RecordFilterOperand.IS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { eq: parseFloat(recordFilter.value), } as FloatFilter, }; case RecordFilterOperand.IS_NOT: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { eq: parseFloat(recordFilter.value), } as FloatFilter, }, @@ -571,7 +584,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS: return { - [correspondingFieldMetadataItem.name + 'Id']: { + [fieldMetadataItem.name + 'Id']: { in: recordIds, } as RelationFilter, }; @@ -581,13 +594,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name + 'Id']: { + [fieldMetadataItem.name + 'Id']: { in: recordIds, } as RelationFilter, }, }, { - [correspondingFieldMetadataItem.name + 'Id']: { + [fieldMetadataItem.name + 'Id']: { is: 'NULL', } as RelationFilter, }, @@ -615,7 +628,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (parsedCurrencyCodes.length === 0) return undefined; const gqlFilter: RecordGqlOperationFilter = { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { currencyCode: { in: parsedCurrencyCodes }, } as CurrencyFilter, }; @@ -643,26 +656,26 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.GREATER_THAN_OR_EQUAL: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { amountMicros: { gte: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; case RecordFilterOperand.LESS_THAN_OR_EQUAL: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { amountMicros: { lte: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; case RecordFilterOperand.IS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { amountMicros: { eq: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; case RecordFilterOperand.IS_NOT: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { amountMicros: { eq: parseFloat(recordFilter.value) * 1000000, }, @@ -682,7 +695,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } case 'LINKS': { return computeGqlOperationFilterForLinks({ - correspondingFieldMetadataItem, + correspondingFieldMetadataItem: fieldMetadataItem, recordFilter, subFieldName, }); @@ -690,7 +703,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ case 'FULL_NAME': { const fullNameFilters = generateILikeFiltersForCompositeFields( recordFilter.value, - correspondingFieldMetadataItem.name, + fieldMetadataItem.name, ['firstName', 'lastName'], ); switch (recordFilter.operand) { @@ -701,7 +714,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }; } else { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { [subFieldName]: { ilike: `%${recordFilter.value}%`, }, @@ -720,7 +733,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } else { return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { [subFieldName]: { ilike: `%${recordFilter.value}%`, }, @@ -741,42 +754,42 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { or: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressStreet1: { ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressStreet2: { ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCity: { ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressState: { ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCountry: { ilike: `%${recordFilter.value}%`, }, } as AddressFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressPostcode: { ilike: `%${recordFilter.value}%`, }, @@ -795,7 +808,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { [subFieldName]: { in: parsedCountryCodes, } as AddressFilter, @@ -804,7 +817,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { [subFieldName]: { ilike: `%${recordFilter.value}%`, } as AddressFilter, @@ -819,7 +832,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressStreet1: { ilike: `%${recordFilter.value}%`, }, @@ -827,7 +840,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressStreet1: { is: 'NULL', }, @@ -839,7 +852,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressStreet2: { ilike: `%${recordFilter.value}%`, }, @@ -847,7 +860,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressStreet2: { is: 'NULL', }, @@ -859,7 +872,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCity: { ilike: `%${recordFilter.value}%`, }, @@ -867,7 +880,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCity: { is: 'NULL', }, @@ -879,7 +892,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressState: { ilike: `%${recordFilter.value}%`, }, @@ -887,7 +900,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressState: { is: 'NULL', }, @@ -899,7 +912,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressPostcode: { ilike: `%${recordFilter.value}%`, }, @@ -907,7 +920,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressPostcode: { is: 'NULL', }, @@ -919,7 +932,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCountry: { ilike: `%${recordFilter.value}%`, }, @@ -927,7 +940,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCountry: { is: 'NULL', }, @@ -954,7 +967,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCountry: { in: JSON.parse(recordFilter.value), } as AddressFilter, @@ -962,7 +975,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { addressCountry: { is: 'NULL', } as AddressFilter, @@ -976,7 +989,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { [subFieldName]: { ilike: `%${recordFilter.value}%`, } as AddressFilter, @@ -984,7 +997,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { [subFieldName]: { is: 'NULL', } as AddressFilter, @@ -1012,7 +1025,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (nonEmptyOptions.length > 0) { conditions.push({ - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { containsAny: nonEmptyOptions, } as MultiSelectFilter, }); @@ -1020,7 +1033,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (emptyOptions.length > 0) { conditions.push({ - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { isEmptyArray: true, } as MultiSelectFilter, }); @@ -1033,18 +1046,18 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { containsAny: nonEmptyOptions, } as MultiSelectFilter, }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { isEmptyArray: true, } as MultiSelectFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { is: 'NULL', } as MultiSelectFilter, }, @@ -1070,7 +1083,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (nonEmptyOptions.length > 0) { conditions.push({ - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { in: nonEmptyOptions, } as SelectFilter, }); @@ -1078,7 +1091,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (emptyOptions.length > 0) { conditions.push({ - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { is: 'NULL', } as SelectFilter, }); @@ -1092,7 +1105,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (nonEmptyOptions.length > 0) { conditions.push({ not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { in: nonEmptyOptions, } as SelectFilter, }, @@ -1102,7 +1115,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ if (emptyOptions.length > 0) { conditions.push({ not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { is: 'NULL', } as SelectFilter, }, @@ -1121,14 +1134,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { containsIlike: `%${recordFilter.value}%`, } as ArrayFilter, }; case RecordFilterOperand.DOES_NOT_CONTAIN: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { containsIlike: `%${recordFilter.value}%`, } as ArrayFilter, }, @@ -1150,7 +1163,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ const parsedSources = JSON.parse(recordFilter.value) as string[]; return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { source: { in: parsedSources, } satisfies RelationFilter, @@ -1168,7 +1181,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { source: { in: parsedSources, } satisfies RelationFilter, @@ -1177,12 +1190,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }; } default: { - const fieldForRecordFilter = fieldMetadataItems.find( - (field) => field.id === recordFilter.fieldMetadataId, - ); - throw new Error( - `Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`, + `Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`, ); } } @@ -1213,7 +1222,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { workspaceMemberId: { in: workspaceMemberIds, } satisfies UUIDFilter, @@ -1224,7 +1233,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { workspaceMemberId: { in: workspaceMemberIds, } satisfies UUIDFilter, @@ -1232,7 +1241,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { workspaceMemberId: { is: 'NULL', } satisfies UUIDFilter, @@ -1242,12 +1251,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }; } default: { - const fieldForRecordFilter = fieldMetadataItems.find( - (field) => field.id === recordFilter.fieldMetadataId, - ); - throw new Error( - `Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`, + `Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`, ); } } @@ -1263,7 +1268,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { or: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { name: { ilike: `%${recordFilter.value}%`, }, @@ -1272,7 +1277,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ ...(matchingSourceValues.length > 0 ? [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { source: { in: matchingSourceValues, }, @@ -1288,7 +1293,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ and: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { name: { ilike: `%${recordFilter.value}%`, }, @@ -1299,7 +1304,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ ? [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { source: { in: matchingSourceValues, }, @@ -1312,19 +1317,15 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }; } default: { - const fieldForRecordFilter = fieldMetadataItems.find( - (field) => field.id === recordFilter.fieldMetadataId, - ); - throw new Error( - `Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`, + `Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`, ); } } } case 'EMAILS': { return computeGqlOperationFilterForEmails({ - correspondingFieldMetadataItem, + correspondingFieldMetadataItem: fieldMetadataItem, recordFilter, subFieldName, }); @@ -1342,21 +1343,21 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { or: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, } as PhonesFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, } as PhonesFilter, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1369,7 +1370,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ and: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, @@ -1378,7 +1379,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, @@ -1389,7 +1390,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1397,7 +1398,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { additionalPhones: { is: 'NULL', } as PhonesFilter, @@ -1423,7 +1424,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ return { or: [ { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1436,7 +1437,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ or: [ { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { additionalPhones: { like: `%${filterValue}%`, }, @@ -1444,7 +1445,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ }, }, { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { additionalPhones: { is: 'NULL', } as PhonesFilter, @@ -1462,7 +1463,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, @@ -1471,7 +1472,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ case RecordFilterOperand.DOES_NOT_CONTAIN: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneNumber: { ilike: `%${filterValue}%`, }, @@ -1488,7 +1489,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.CONTAINS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, @@ -1497,7 +1498,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ case RecordFilterOperand.DOES_NOT_CONTAIN: return { not: { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { primaryPhoneCallingCode: { ilike: `%${filterValue}%`, }, @@ -1518,7 +1519,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } case 'BOOLEAN': { return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { eq: recordFilter.value === 'true', } as BooleanFilter, }; @@ -1531,7 +1532,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ switch (recordFilter.operand) { case RecordFilterOperand.IS: return { - [correspondingFieldMetadataItem.name]: { + [fieldMetadataItem.name]: { in: recordIds, } as UUIDFilter, };