diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts index 14f342b251..6b1eb722bc 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts @@ -535,4 +535,96 @@ describe('isRecordMatchingFilter', () => { ).toBe(false); }); }); + + describe('Relation Filters', () => { + const accountOwnerId = '20202020-0687-4c41-b707-ed1bfca972a7'; + + const companyWithAccountOwner = { + ...companiesMock[0], + accountOwner: { id: accountOwnerId }, + accountOwnerId, + }; + + const companyWithoutAccountOwner = { + ...companyWithAccountOwner, + accountOwner: null, + accountOwnerId: null, + }; + + it('matches "is not empty" on a relation field by its related record id', () => { + const filter: RecordGqlOperationFilter = { + accountOwner: { is: 'NOT_NULL' }, + }; + + expect( + isRecordMatchingFilter({ + record: companyWithAccountOwner, + filter, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(true); + + expect( + isRecordMatchingFilter({ + record: companyWithoutAccountOwner, + filter, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(false); + }); + + it('matches "is empty" on a relation field by its related record id', () => { + const filter: RecordGqlOperationFilter = { + accountOwner: { is: 'NULL' }, + }; + + expect( + isRecordMatchingFilter({ + record: companyWithoutAccountOwner, + filter, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(true); + + expect( + isRecordMatchingFilter({ + record: companyWithAccountOwner, + filter, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(false); + }); + + it('matches an "in" filter on a relation field by its related record id', () => { + expect( + isRecordMatchingFilter({ + record: companyWithAccountOwner, + filter: { accountOwner: { in: [accountOwnerId] } }, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(true); + + expect( + isRecordMatchingFilter({ + record: companyWithAccountOwner, + filter: { accountOwner: { in: ['unknown-id'] } }, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(false); + }); + + it('still matches the relation join column field', () => { + const filter: RecordGqlOperationFilter = { + accountOwnerId: { is: 'NOT_NULL' }, + }; + + expect( + isRecordMatchingFilter({ + record: companyWithAccountOwner, + filter, + objectMetadataItem: companyMockObjectMetadataItem, + }), + ).toBe(true); + }); + }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index fae0faa2df..66d5712050 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -434,9 +434,10 @@ export const isRecordMatchingFilter = ({ }); } - throw new Error( - `Not implemented yet, use UUID filter instead on the corresponding "${filterKey}Id" field`, - ); + return isMatchingUUIDFilter({ + uuidFilter: filterValue as UUIDFilter, + value: record[filterKey]?.id ?? null, + }); } case FieldMetadataType.TS_VECTOR: { return isMatchingTSVectorFilter({ diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts index cbeff80d97..36ef6e3cbf 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts @@ -286,4 +286,44 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => { expect(result).toBe(true); }); + + it('matches "is not empty" on a relation field by its related record id', () => { + expect( + isRecordMatchingRLSRowLevelPermissionPredicate({ + record: { ...baseRecord, company: { id: 'company-1' } } as ObjectRecord, + filter: { company: { is: 'NOT_NULL' } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ).toBe(true); + + expect( + isRecordMatchingRLSRowLevelPermissionPredicate({ + record: { ...baseRecord, company: null } as ObjectRecord, + filter: { company: { is: 'NOT_NULL' } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ).toBe(false); + }); + + it('matches "is empty" on a relation field by its related record id', () => { + expect( + isRecordMatchingRLSRowLevelPermissionPredicate({ + record: { ...baseRecord, company: null } as ObjectRecord, + filter: { company: { is: 'NULL' } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ).toBe(true); + + expect( + isRecordMatchingRLSRowLevelPermissionPredicate({ + record: { ...baseRecord, company: { id: 'company-1' } } as ObjectRecord, + filter: { company: { is: 'NULL' } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ).toBe(false); + }); }); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts index 6b0cffc50c..39070baae9 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts @@ -426,9 +426,10 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({ }); } - throw new Error( - `Not implemented yet, use UUID filter instead on the corresponding "${filterKey}Id" field`, - ); + return isMatchingUUIDFilter({ + uuidFilter: filterValue as UUIDFilter, + value: recordFieldValue?.id ?? null, + }); } case FieldMetadataType.TS_VECTOR: { return isMatchingTSVectorFilter({