Fix client-side record matching for nested relation filters (#23832)

## Problem

`isRecordMatchingFilter` assumes every filter keyed by a relation field
name is a flat UUID filter on the related record id. Filters that
traverse a relation, like

```
{ pointOfContact: { companyId: { in: [companyId] } } }
```

(produced by view filters carrying `relationTargetFieldMetadataId`, such
as the seeded filter of a nested relation Field widget in #23815), make
it throw `Unexpected value for UUID filter`. The throw happens inside
the create and update optimistic effects, so creating a record from a
view seeded with such a filter aborts before anything is written.

## Fix

When the value under a relation field name holds related record field
names (or `and`/`or`/`not` composites) instead of UUID operators,
recurse into the related record with the relation target's object
metadata. A related record missing from the payload, or a list relation,
conservatively does not match. Flat UUID filters on the relation name,
join column filters and morph relations keep their existing behavior.

`isRecordMatchingFilter` now takes `objectMetadataItems` to resolve the
relation target metadata. The optimistic effect call sites already had
it in scope; it is threaded through the two group-by helpers.

## Tests

- New `Nested Relation Filters` cases: match, no match, related record
not loaded, composite conditions, list relation.
- Existing suites updated for the added parameter; record-filter and
optimistic-effect suites green, typecheck green.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Xp3AgGtc4kSP8PpgpKMWLQ)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23832?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Félix Malfait
2026-08-05 21:10:49 +02:00
committed by GitHub
parent 5ada3adfd5
commit c3d0f021b7
10 changed files with 334 additions and 4 deletions
@@ -55,6 +55,7 @@ describe('processGroupByConnectionWithRecords', () => {
groupByDimensionValues: [],
groupByConfig: undefined,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
readField: mockReadField,
toReference: mockToReference,
});
@@ -80,6 +81,7 @@ describe('processGroupByConnectionWithRecords', () => {
groupByDimensionValues: [],
groupByConfig: undefined,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
readField: mockReadField,
toReference: mockToReference,
});
@@ -111,6 +113,7 @@ describe('processGroupByConnectionWithRecords', () => {
groupByDimensionValues: [],
groupByConfig: undefined,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
readField: mockReadField,
toReference: mockToReference,
});
@@ -26,6 +26,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
operation: 'create',
records: [mockRecord],
shouldMatchRootQueryFilter: false,
@@ -48,6 +49,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
operation: 'update',
records: [mockRecord],
shouldMatchRootQueryFilter: false,
@@ -65,6 +67,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
operation: 'delete',
records: [mockRecord],
shouldMatchRootQueryFilter: false,
@@ -82,6 +85,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
objectMetadataItem: mockObjectMetadataItem,
objectMetadataItems: [mockObjectMetadataItem],
operation: 'create',
records: [],
shouldMatchRootQueryFilter: false,
@@ -27,6 +27,7 @@ type ProcessGroupByConnectionWithRecordsArgs = {
| Array<Record<string, boolean | Record<string, string>>>
| Record<string, boolean | Record<string, string>>;
objectMetadataItem: EnrichedObjectMetadataItem;
objectMetadataItems: EnrichedObjectMetadataItem[];
readField: ReadFieldFunction;
toReference: ToReferenceFunction;
};
@@ -41,6 +42,7 @@ export const processGroupByConnectionWithRecords = ({
groupByDimensionValues,
groupByConfig,
objectMetadataItem,
objectMetadataItems,
readField,
toReference,
}: ProcessGroupByConnectionWithRecordsArgs): {
@@ -62,6 +64,7 @@ export const processGroupByConnectionWithRecords = ({
record,
filter: queryFilter ?? {},
objectMetadataItem,
objectMetadataItems,
});
const belongsToGroup = doesRecordBelongToGroup(
@@ -17,6 +17,7 @@ import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
type TriggerUpdateGroupByQueriesOptimisticEffectArgs = {
cache: ApolloCache;
objectMetadataItem: EnrichedObjectMetadataItem;
objectMetadataItems: EnrichedObjectMetadataItem[];
operation: 'create' | 'update' | 'delete';
records: RecordGqlNode[];
shouldMatchRootQueryFilter?: boolean;
@@ -25,6 +26,7 @@ type TriggerUpdateGroupByQueriesOptimisticEffectArgs = {
export const triggerUpdateGroupByQueriesOptimisticEffect = ({
cache,
objectMetadataItem,
objectMetadataItems,
operation,
records,
shouldMatchRootQueryFilter = false,
@@ -86,6 +88,7 @@ export const triggerUpdateGroupByQueriesOptimisticEffect = ({
: [],
groupByConfig,
objectMetadataItem,
objectMetadataItems,
readField,
toReference,
});
@@ -122,6 +125,7 @@ export const triggerUpdateGroupByQueriesOptimisticEffect = ({
record,
filter: queryFilter ?? {},
objectMetadataItem,
objectMetadataItems,
});
if (
@@ -135,6 +135,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
record: recordToCreate,
filter: rootQueryFilter,
objectMetadataItem,
objectMetadataItems,
})
) {
return [];
@@ -200,6 +201,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
triggerUpdateGroupByQueriesOptimisticEffect({
cache,
objectMetadataItem,
objectMetadataItems,
operation: 'create',
records: recordsToCreate,
shouldMatchRootQueryFilter,
@@ -63,6 +63,7 @@ export const triggerDestroyRecordsOptimisticEffect = ({
record,
filter: rootQueryVariables?.filter ?? {},
objectMetadataItem,
objectMetadataItems,
}),
);
@@ -129,6 +130,7 @@ export const triggerDestroyRecordsOptimisticEffect = ({
triggerUpdateGroupByQueriesOptimisticEffect({
cache,
objectMetadataItem,
objectMetadataItems,
operation: 'delete',
records: recordsToDestroy,
});
@@ -79,12 +79,14 @@ export const triggerUpdateRecordOptimisticEffect = ({
record: updatedRecord,
filter: rootQueryFilter ?? {},
objectMetadataItem,
objectMetadataItems,
});
const currentRecordIndexInRootQueryEdges = isRecordMatchingFilter({
record: currentRecord,
filter: rootQueryFilter ?? {},
objectMetadataItem,
objectMetadataItems,
});
const totalCount = readField<number | undefined>(
@@ -160,6 +162,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
triggerUpdateGroupByQueriesOptimisticEffect({
cache,
objectMetadataItem,
objectMetadataItems,
operation: 'update',
records: [updatedRecord],
shouldMatchRootQueryFilter: true,
@@ -84,6 +84,7 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
record: updatedRecord,
filter: rootQueryFilter ?? {},
objectMetadataItem,
objectMetadataItems,
});
const updatedRecordFoundInRootQueryEdges = isDefined(
@@ -145,6 +146,7 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
triggerUpdateGroupByQueriesOptimisticEffect({
cache,
objectMetadataItem,
objectMetadataItems,
operation: 'update',
records: updatedRecords,
shouldMatchRootQueryFilter: true,
@@ -11,10 +11,11 @@ const companiesMock = mockedCompanyRecords.map((record) =>
getRecordFromRecordNode<Company>({ recordNode: record }),
);
const companyMockObjectMetadataItem =
getTestEnrichedObjectMetadataItemsMock().find(
(item) => item.nameSingular === 'company',
)!;
const objectMetadataItems = getTestEnrichedObjectMetadataItemsMock();
const companyMockObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
describe('isRecordMatchingFilter', () => {
describe('Empty Filters', () => {
@@ -27,6 +28,7 @@ describe('isRecordMatchingFilter', () => {
record: company,
filter: emptyFilter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
@@ -44,6 +46,7 @@ describe('isRecordMatchingFilter', () => {
record: company,
filter: filterWithEmptyFields,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
@@ -58,6 +61,7 @@ describe('isRecordMatchingFilter', () => {
record: company,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
@@ -72,6 +76,7 @@ describe('isRecordMatchingFilter', () => {
record: company,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
@@ -86,6 +91,7 @@ describe('isRecordMatchingFilter', () => {
record: company,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
@@ -110,6 +116,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -118,6 +125,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -148,6 +156,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
expect(
@@ -155,6 +164,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -179,6 +189,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -187,6 +198,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -213,6 +225,7 @@ describe('isRecordMatchingFilter', () => {
record: companyIdealCustomerProfileTrue,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(companyIdealCustomerProfileTrue.idealCustomerProfile);
expect(
@@ -220,6 +233,7 @@ describe('isRecordMatchingFilter', () => {
record: companyIdealCustomerProfileFalse,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(companyIdealCustomerProfileFalse.idealCustomerProfile);
});
@@ -270,6 +284,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -278,6 +293,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -313,6 +329,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
@@ -321,6 +338,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
@@ -365,6 +383,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -373,6 +392,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -405,6 +425,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -413,6 +434,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -443,6 +465,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -451,6 +474,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -484,6 +508,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true); // Matches as Airbnb's employee count is between 10 and 100000
@@ -492,6 +517,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false); // Does not match as Aircall's employee count is not within the range
});
@@ -523,6 +549,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -531,6 +558,7 @@ describe('isRecordMatchingFilter', () => {
record: companyMockNotInFilter,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -561,6 +589,7 @@ describe('isRecordMatchingFilter', () => {
record: companyWithAccountOwner,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -569,6 +598,7 @@ describe('isRecordMatchingFilter', () => {
record: companyWithoutAccountOwner,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -583,6 +613,7 @@ describe('isRecordMatchingFilter', () => {
record: companyWithoutAccountOwner,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -591,6 +622,7 @@ describe('isRecordMatchingFilter', () => {
record: companyWithAccountOwner,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -601,6 +633,7 @@ describe('isRecordMatchingFilter', () => {
record: companyWithAccountOwner,
filter: { accountOwner: { in: [accountOwnerId] } },
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
@@ -609,6 +642,7 @@ describe('isRecordMatchingFilter', () => {
record: companyWithAccountOwner,
filter: { accountOwner: { in: ['unknown-id'] } },
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
@@ -623,8 +657,189 @@ describe('isRecordMatchingFilter', () => {
record: companyWithAccountOwner,
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
});
describe('Nested Relation Filters', () => {
const opportunityMockObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === 'opportunity',
)!;
const companyId = '20202020-171e-4bcc-9cf7-43448d6fb278';
const personId = '20202020-2d40-4e49-8df4-9c6a049190ef';
const opportunityWithPointOfContact = {
id: '20202020-83f4-4c4f-95c1-b7be9f2d36d1',
pointOfContact: { id: personId, companyId },
};
it('matches when the related record satisfies the nested filter', () => {
const filter = {
pointOfContact: { companyId: { in: [companyId] } },
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: opportunityWithPointOfContact,
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
it('does not match when the related record fails the nested filter', () => {
const filter = {
pointOfContact: {
companyId: { in: ['20202020-0000-4000-8000-000000000000'] },
},
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: opportunityWithPointOfContact,
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
it('does not match when the related record is not loaded', () => {
const filter = {
pointOfContact: { companyId: { in: [companyId] } },
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: {
...opportunityWithPointOfContact,
pointOfContact: null,
},
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
expect(
isRecordMatchingFilter({
record: { id: opportunityWithPointOfContact.id },
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
it('evaluates composite conditions against the related record', () => {
const filter = {
pointOfContact: {
and: [{ companyId: { in: [companyId] } }, { id: { eq: personId } }],
},
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: opportunityWithPointOfContact,
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
const nonMatchingFilter = {
pointOfContact: {
and: [{ companyId: { in: [companyId] } }, { id: { neq: personId } }],
},
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: opportunityWithPointOfContact,
filter: nonMatchingFilter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
it('keeps a record with an unloaded relation out of a negated nested filter', () => {
const filter = {
not: { pointOfContact: { companyId: { in: [companyId] } } },
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: { id: opportunityWithPointOfContact.id },
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
it('evaluates a negated nested filter truthfully on a loaded relation', () => {
const filter = {
not: { pointOfContact: { companyId: { in: [companyId] } } },
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: opportunityWithPointOfContact,
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
expect(
isRecordMatchingFilter({
record: {
...opportunityWithPointOfContact,
pointOfContact: {
id: personId,
companyId: '20202020-0000-4000-8000-000000000000',
},
},
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
expect(
isRecordMatchingFilter({
record: {
...opportunityWithPointOfContact,
pointOfContact: null,
},
filter,
objectMetadataItem: opportunityMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(true);
});
it('does not match a nested filter on a list of related records', () => {
const filter = {
people: { id: { eq: personId } },
} as RecordGqlOperationFilter;
expect(
isRecordMatchingFilter({
record: {
...companiesMock[0],
people: [{ id: personId }],
},
filter,
objectMetadataItem: companyMockObjectMetadataItem,
objectMetadataItems,
}),
).toBe(false);
});
});
});
@@ -94,14 +94,83 @@ const isNotFilter = (
filter: RecordGqlOperationFilter,
): filter is NotObjectRecordFilter => 'not' in filter && !!filter.not;
const UUID_FILTER_OPERATOR_KEYS = new Set<string>([
'eq',
'gt',
'gte',
'in',
'is',
'lt',
'lte',
'neq',
]);
// A filter on a relation field name either holds UUID operators applied to
// the related record id, or field names of the related record to match
// against the related record itself, like { person: { companyId: { in: [...] } } }
// produced by view filters traversing a relation.
const isNestedRelationFilter = (
filterValue: unknown,
): filterValue is RecordGqlOperationFilter =>
isObject(filterValue) &&
Object.keys(filterValue).some((key) => !UUID_FILTER_OPERATOR_KEYS.has(key));
const isRecordMatchingNestedRelationFilter = ({
relationRecord,
nestedFilter,
relationFieldMetadataItem,
objectMetadataItems,
isWithinNegatedFilter,
}: {
relationRecord: unknown;
nestedFilter: RecordGqlOperationFilter;
relationFieldMetadataItem: Pick<FieldMetadataItem, 'relation'>;
objectMetadataItems: EnrichedObjectMetadataItem[];
isWithinNegatedFilter: boolean;
}): boolean => {
// A null related record truthfully fails the nested predicate, matching
// the backend NOT EXISTS semantics. A related record that was not loaded
// leaves the outcome unknown: returning the negation parity keeps the
// record excluded whether or not a surrounding not flips the result.
if (relationRecord === null) {
return false;
}
if (!isObject(relationRecord) || Array.isArray(relationRecord)) {
return isWithinNegatedFilter;
}
const relationTargetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
relationFieldMetadataItem.relation?.targetObjectMetadata.id,
);
if (!isDefined(relationTargetObjectMetadataItem)) {
return isWithinNegatedFilter;
}
return isRecordMatchingFilter({
record: relationRecord,
filter: nestedFilter,
objectMetadataItem: relationTargetObjectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter,
});
};
export const isRecordMatchingFilter = ({
record,
filter,
objectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter = false,
}: {
record: any;
filter: RecordGqlOperationFilter;
objectMetadataItem: EnrichedObjectMetadataItem;
objectMetadataItems: EnrichedObjectMetadataItem[];
isWithinNegatedFilter?: boolean;
}): boolean => {
if (Object.keys(filter).length === 0 && record.deletedAt === null) {
return true;
@@ -113,6 +182,8 @@ export const isRecordMatchingFilter = ({
record,
filter: { [filterKey]: value },
objectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter,
}),
);
}
@@ -133,6 +204,8 @@ export const isRecordMatchingFilter = ({
record,
filter: andFilter,
objectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter,
}),
)
);
@@ -149,6 +222,8 @@ export const isRecordMatchingFilter = ({
record,
filter: orFilter,
objectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter,
}),
)
);
@@ -160,6 +235,8 @@ export const isRecordMatchingFilter = ({
record,
filter: filterValue,
objectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter,
});
}
@@ -179,6 +256,8 @@ export const isRecordMatchingFilter = ({
record,
filter: filterValue,
objectMetadataItem,
objectMetadataItems,
isWithinNegatedFilter: !isWithinNegatedFilter,
})
);
}
@@ -434,6 +513,19 @@ export const isRecordMatchingFilter = ({
});
}
if (
objectMetadataField.type === FieldMetadataType.RELATION &&
isNestedRelationFilter(filterValue)
) {
return isRecordMatchingNestedRelationFilter({
relationRecord: record[filterKey],
nestedFilter: filterValue,
relationFieldMetadataItem: objectMetadataField,
objectMetadataItems,
isWithinNegatedFilter,
});
}
return isMatchingUUIDFilter({
uuidFilter: filterValue as UUIDFilter,
value: record[filterKey]?.id ?? null,