fix(filters): make filter dispatcher own relation-target resolution (#20670)
## Summary Two relation-traversal bugs surfaced post-merge of #20533, both rooted in the same architectural smell: the GraphQL filter dispatcher took a flat `fields: FieldShared[]` array and silently dropped any filter whose `relationTargetFieldMetadataId` wasn't in that array. Callers had to remember to pre-augment the list with relation targets — and 16+ call sites did not all know this. This PR fixes both bugs and removes the smell. ### Bug 1 — Save as new view loses the relation target `useCreateViewFromCurrentView` built the create-filter input without `relationTargetFieldMetadataId`. The saved view's filter persisted without the traversal — on reload the chip showed "Company contains 'air'" instead of "Company → Name contains 'air'". Discarded at save time, not at read time. Fix: include `relationTargetFieldMetadataId` in the create input. (Commit 1.) ### Bug 2 — Workflow Search Records drops one-hop traversals `FindRecordsWorkflowAction` built its fields list from `flatObjectMetadata.fieldIds` only (source object's fields). The shared dispatcher then couldn't resolve the relation target field on the related object and silently dropped the filter — a configured "People where Company → Name Contains 'Airbnb'" came through as `{ and: [] }`. This was the same shape as bugs already fixed in 5 other call sites (chart filters, view filters, record table, etc.). The pattern was: caller forgets to augment fields → dispatcher silently drops the filter. Fix (commit 2): change the dispatcher to take a `findFieldMetadataItemById: (id) => FieldShared | undefined` resolver callback. Both source-field and relation-target-field lookups go through the same resolver, so callers no longer need to know about the augmentation requirement. Frontend callers pass a workspace-wide resolver built from `flattenedFieldMetadataItemsSelector`; server callers wrap `findFlatEntityByIdInFlatEntityMaps` on `flatFieldMetadataMaps`. In both cases relation-target lookups just work, because the resolver can see fields on related objects. ## Why this matters Before: "if you call the dispatcher, pre-augment your fields list with relation targets, or filters get silently dropped." An invariant only enforceable by code review, broken often enough to ship two user-visible bugs in one week. After: the dispatcher resolves field ids itself. There's no list to forget to augment. The failure mode (filter silently dropped) becomes structurally impossible at the dispatcher boundary. Net diff: 240 insertions, 319 deletions. Removed `augmentFieldsWithRelationTargets` (frontend) and the workflow whack-a-mole code (server). ## Test plan - [ ] Save view: create an advanced filter using a one-hop relation traversal, click "Save as new view", reload, confirm the chip still reads "Source → Target operator value" - [ ] Workflow: configure a Search Records action with a relation-traversal filter, run the workflow, confirm the filter is actually applied - [ ] Dashboard chart: configure a chart with a relation-traversal filter, confirm the chart data respects it - [ ] Record table, group-by, calendar, total count, footer aggregates: all continue to work with both plain and relation-traversal filters
This commit is contained in:
+2
-1
@@ -27,7 +27,8 @@ describe('computeRecordGqlOperationFilter', () => {
|
||||
];
|
||||
|
||||
const filter = computeRecordGqlOperationFilter({
|
||||
fields: [companyIdField],
|
||||
findFieldMetadataItemById: (id) =>
|
||||
id === companyIdField.id ? companyIdField : undefined,
|
||||
recordFilters,
|
||||
recordFilterGroups: [],
|
||||
filterValueDependencies: {
|
||||
|
||||
+7
-4
@@ -21,11 +21,14 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fieldById = new Map(fields.map((field) => [field.id, field]));
|
||||
const findFieldMetadataItemById = (id: string) => fieldById.get(id);
|
||||
|
||||
it('should return undefined when group is not found', () => {
|
||||
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies: {},
|
||||
filters: [],
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups: [],
|
||||
currentRecordFilterGroupId: 'nonexistent',
|
||||
});
|
||||
@@ -45,7 +48,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
|
||||
recordFilterGroupId: 'group1',
|
||||
},
|
||||
],
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: 'group1',
|
||||
@@ -70,7 +73,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
|
||||
recordFilterGroupId: 'group1',
|
||||
},
|
||||
],
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: 'group1',
|
||||
@@ -95,7 +98,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
|
||||
recordFilterGroupId: 'subgroup1',
|
||||
},
|
||||
],
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: 'group1',
|
||||
|
||||
+59
-56
@@ -131,6 +131,9 @@ const fields = [
|
||||
|
||||
const filterValueDependencies = { timeZone: 'UTC' };
|
||||
|
||||
const fieldById = new Map(fields.map((field) => [field.id, field]));
|
||||
const findFieldMetadataItemById = (id: string) => fieldById.get(id);
|
||||
|
||||
const makeFilter = (
|
||||
fieldMetadataId: string,
|
||||
operand: RecordFilterOperand,
|
||||
@@ -156,7 +159,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'x',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -166,7 +169,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-text', RecordFilterOperand.CONTAINS, ''),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -181,7 +184,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ name: { ilike: '%test%' } });
|
||||
@@ -195,7 +198,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ not: { name: { ilike: '%test%' } } });
|
||||
@@ -207,7 +210,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-number', RecordFilterOperand.IS, '42'),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ amount: { eq: 42 } });
|
||||
@@ -217,7 +220,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-number', RecordFilterOperand.IS_NOT, '42'),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ not: { amount: { eq: 42 } } });
|
||||
@@ -231,7 +234,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
'10',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ amount: { gte: 10 } });
|
||||
@@ -245,7 +248,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
'100',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ amount: { lte: 100 } });
|
||||
@@ -261,7 +264,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_AFTER,
|
||||
'2024-03-15',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ createdAt: { gte: '2024-03-15' } });
|
||||
@@ -275,7 +278,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_BEFORE,
|
||||
'2024-03-15',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ createdAt: { lt: '2024-03-15' } });
|
||||
@@ -289,7 +292,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS,
|
||||
'2024-03-15',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ createdAt: { eq: '2024-03-15' } });
|
||||
@@ -299,7 +302,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_IN_PAST, ''),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('createdAt.lt');
|
||||
@@ -313,7 +316,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_IN_FUTURE,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('createdAt.gte');
|
||||
@@ -323,7 +326,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_TODAY, ''),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('createdAt.eq');
|
||||
@@ -337,7 +340,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_RELATIVE,
|
||||
'PAST_7_DAY',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
@@ -353,7 +356,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_AFTER,
|
||||
'2024-03-15T10:00:00Z',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.gte');
|
||||
@@ -367,7 +370,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_BEFORE,
|
||||
'2024-03-15T10:00:00Z',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.lt');
|
||||
@@ -381,7 +384,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS,
|
||||
'2024-03-15T10:00:00Z',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
@@ -395,7 +398,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_IN_PAST,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.lt');
|
||||
@@ -409,7 +412,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_IN_FUTURE,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.gt');
|
||||
@@ -423,7 +426,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_TODAY,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
@@ -437,7 +440,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_RELATIVE,
|
||||
`PAST_7_DAY;;UTC;;`,
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
@@ -449,7 +452,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-rating', RecordFilterOperand.IS, '3'),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('rating.eq');
|
||||
@@ -463,7 +466,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
'3',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('rating.in');
|
||||
@@ -477,7 +480,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
'3',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('rating.in');
|
||||
@@ -489,7 +492,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'true'),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ isActive: { eq: true } });
|
||||
@@ -499,7 +502,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'false'),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ isActive: { eq: false } });
|
||||
@@ -515,7 +518,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS,
|
||||
'["ACTIVE","PENDING"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('status.in');
|
||||
@@ -529,7 +532,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_NOT,
|
||||
'["ACTIVE"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('not');
|
||||
@@ -545,7 +548,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'["TAG1","TAG2"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('tags.containsAny');
|
||||
@@ -559,7 +562,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'["TAG1"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
@@ -575,7 +578,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('companyId.in');
|
||||
@@ -589,7 +592,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS_NOT,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
@@ -605,7 +608,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ metadata: { like: '%test%' } });
|
||||
@@ -619,7 +622,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ not: { metadata: { like: '%test%' } } });
|
||||
@@ -635,7 +638,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'doc',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ attachments: { like: '%doc%' } });
|
||||
@@ -651,7 +654,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.VECTOR_SEARCH,
|
||||
'hello world',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ search: { search: 'hello world' } });
|
||||
@@ -669,7 +672,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
'CURRENCY',
|
||||
'amountMicros',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('revenue');
|
||||
@@ -685,7 +688,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'John',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
@@ -701,7 +704,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'Paris',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
@@ -717,7 +720,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'api',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -744,7 +747,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'xyz123',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -769,7 +772,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'api',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -800,7 +803,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'xyz123',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -831,7 +834,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
'PHONES',
|
||||
'primaryPhoneNumber',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@@ -849,7 +852,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
'EMAILS',
|
||||
'primaryEmail',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@@ -867,7 +870,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
'LINKS',
|
||||
'primaryLinkUrl',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@@ -883,7 +886,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'["item1"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@@ -897,7 +900,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'["item1"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('not');
|
||||
@@ -913,7 +916,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('recordId.in');
|
||||
@@ -931,13 +934,13 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
|
||||
relationTargetFieldMetadataId: 'f-text',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ company: { name: { ilike: '%Acme%' } } });
|
||||
});
|
||||
|
||||
// If the target field is no longer in fieldMetadataItems (e.g. it was
|
||||
// If the target field is no longer resolvable (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.
|
||||
@@ -948,7 +951,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
|
||||
relationTargetFieldMetadataId: 'nonexistent-target',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -963,7 +966,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
...makeFilter('f-relation', RecordFilterOperand.IS_EMPTY, ''),
|
||||
relationTargetFieldMetadataId: 'f-text',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
// Without traversal, the RELATION case would have produced a filter
|
||||
@@ -984,7 +987,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
...makeFilter('f-relation', RecordFilterOperand.IS, '["ACTIVE"]'),
|
||||
relationTargetFieldMetadataId: 'f-select',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ company: { status: { in: ['ACTIVE'] } } });
|
||||
@@ -1001,7 +1004,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('companyId.in');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type PartialFieldMetadataItem,
|
||||
type RecordFilterValueDependencies,
|
||||
type RecordGqlOperationFilter,
|
||||
} from '@/types';
|
||||
@@ -8,17 +7,20 @@ import {
|
||||
type RecordFilter,
|
||||
type RecordFilterGroup,
|
||||
} from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
||||
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
import {
|
||||
type FindFieldMetadataItemById,
|
||||
turnRecordFilterIntoRecordGqlOperationFilter,
|
||||
} from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
import { isDefined } from '@/utils/validation/isDefined';
|
||||
|
||||
export const computeRecordGqlOperationFilter = ({
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilters,
|
||||
recordFilterGroups,
|
||||
filterValueDependencies,
|
||||
}: {
|
||||
recordFilters: Omit<RecordFilter, 'id'>[];
|
||||
fields: PartialFieldMetadataItem[];
|
||||
findFieldMetadataItemById: FindFieldMetadataItemById;
|
||||
recordFilterGroups: RecordFilterGroup[];
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
}): RecordGqlOperationFilter => {
|
||||
@@ -28,7 +30,7 @@ export const computeRecordGqlOperationFilter = ({
|
||||
.map((regularFilter) => {
|
||||
return turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
recordFilter: regularFilter,
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
filterValueDependencies,
|
||||
});
|
||||
})
|
||||
@@ -42,7 +44,7 @@ export const computeRecordGqlOperationFilter = ({
|
||||
turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
filters: recordFilters,
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups,
|
||||
currentRecordFilterGroupId: outermostFilterGroupId,
|
||||
});
|
||||
|
||||
@@ -221,11 +221,13 @@ export const turnAnyFieldFilterIntoRecordGqlFilter = ({
|
||||
}
|
||||
}
|
||||
|
||||
const fieldById = new Map(fields.map((field) => [field.id, field]));
|
||||
|
||||
const baseRecordGqlOperationFilters = anyFieldRecordFilters
|
||||
.map((recordFilter) =>
|
||||
turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies: {},
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById: (id) => fieldById.get(id),
|
||||
recordFilter,
|
||||
}),
|
||||
)
|
||||
|
||||
+8
-6
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
type CompositeFieldSubFieldName,
|
||||
type FilterableAndTSVectorFieldType,
|
||||
type PartialFieldMetadataItem,
|
||||
RecordFilterGroupLogicalOperator,
|
||||
type RecordFilterValueDependencies,
|
||||
type RecordGqlOperationFilter,
|
||||
@@ -9,7 +8,10 @@ import {
|
||||
} from '@/types';
|
||||
|
||||
import { isDefined } from '@/utils';
|
||||
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
import {
|
||||
type FindFieldMetadataItemById,
|
||||
turnRecordFilterIntoRecordGqlOperationFilter,
|
||||
} from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
|
||||
export type RecordFilter = {
|
||||
id: string;
|
||||
@@ -31,13 +33,13 @@ export type RecordFilterGroup = {
|
||||
export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
|
||||
filterValueDependencies,
|
||||
filters,
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups,
|
||||
currentRecordFilterGroupId,
|
||||
}: {
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
filters: Omit<RecordFilter, 'id'>[];
|
||||
fields: PartialFieldMetadataItem[];
|
||||
findFieldMetadataItemById: FindFieldMetadataItemById;
|
||||
recordFilterGroups: RecordFilterGroup[];
|
||||
currentRecordFilterGroupId?: string;
|
||||
}): RecordGqlOperationFilter | undefined => {
|
||||
@@ -58,7 +60,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
|
||||
turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: recordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
findFieldMetadataItemById,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
@@ -73,7 +75,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
|
||||
turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
filters,
|
||||
fields,
|
||||
findFieldMetadataItemById,
|
||||
recordFilterGroups,
|
||||
currentRecordFilterGroupId: subRecordFilterGroup.id,
|
||||
}),
|
||||
|
||||
@@ -62,21 +62,23 @@ type FieldShared = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type FindFieldMetadataItemById = (id: string) => FieldShared | undefined;
|
||||
|
||||
type TurnRecordFilterIntoRecordGqlOperationFilterParams = {
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
recordFilter: Omit<RecordFilter, 'id'>;
|
||||
fieldMetadataItems: FieldShared[];
|
||||
findFieldMetadataItemById: FindFieldMetadataItemById;
|
||||
};
|
||||
|
||||
export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
recordFilter,
|
||||
fieldMetadataItems,
|
||||
findFieldMetadataItemById,
|
||||
filterValueDependencies,
|
||||
}: TurnRecordFilterIntoRecordGqlOperationFilterParams):
|
||||
| RecordGqlOperationFilter
|
||||
| undefined => {
|
||||
const sourceFieldMetadataItem = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.fieldMetadataId,
|
||||
const sourceFieldMetadataItem = findFieldMetadataItemById(
|
||||
recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(sourceFieldMetadataItem)) {
|
||||
@@ -91,8 +93,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
sourceFieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
isDefined(recordFilter.relationTargetFieldMetadataId)
|
||||
) {
|
||||
const targetFieldMetadataItem = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.relationTargetFieldMetadataId,
|
||||
const targetFieldMetadataItem = findFieldMetadataItemById(
|
||||
recordFilter.relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetFieldMetadataItem)) {
|
||||
|
||||
@@ -104,6 +104,7 @@ export type {
|
||||
RecordFilterGroup,
|
||||
} from './filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
||||
export { turnRecordFilterGroupsIntoGqlOperationFilter } from './filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
||||
export type { FindFieldMetadataItemById } from './filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
export { turnRecordFilterIntoRecordGqlOperationFilter } from './filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
export { combineFilters } from './filter/utils/combineFilters';
|
||||
export { convertViewFilterOperandToCoreOperand } from './filter/utils/convert-view-filter-operand-to-core-operand.util';
|
||||
|
||||
Reference in New Issue
Block a user