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:
Félix Malfait
2026-05-19 11:55:22 +02:00
committed by GitHub
parent a9634b027e
commit 291ce5ccdb
39 changed files with 824 additions and 419 deletions
@@ -9,6 +9,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -89,8 +90,8 @@ export const buildHeadlessCommandContextApi = ({
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
: systemTimeZone;
const flattenedFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
const fieldMetadataItemByIdMap = store.get(
fieldMetadataItemByIdMapSelector.atom,
);
const graphqlFilter = isDefined(objectMetadataItem)
@@ -99,7 +100,7 @@ export const buildHeadlessCommandContextApi = ({
contextStoreFilters: filters,
contextStoreFilterGroups: filterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies: {
currentWorkspaceMemberId: currentWorkspaceMember?.id,
timeZone: userTimezone,
@@ -5,7 +5,7 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
@@ -50,29 +50,22 @@ export const useFindManyRecordsSelectedInContextStore = ({
const { filterValueDependencies } = useFilterValueDependencies();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const allFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const isSoftDeleteFilterActive = contextStoreFilters.some((filter) => {
const foundFieldMetadataItem = allFieldMetadataItems.find(
(fieldMetadataItem) => fieldMetadataItem.id === filter.fieldMetadataId,
);
return (
foundFieldMetadataItem?.name === 'deletedAt' &&
filter.operand === RecordFilterOperand.IS_NOT_EMPTY
);
});
const isSoftDeleteFilterActive = contextStoreFilters.some(
(filter) =>
fieldMetadataItemByIdMap.get(filter.fieldMetadataId)?.name ===
'deletedAt' && filter.operand === RecordFilterOperand.IS_NOT_EMPTY,
);
const queryFilter = computeContextStoreFilters({
contextStoreTargetedRecordsRule,
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems: allFieldMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -29,7 +29,8 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters: [],
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
findFieldMetadataItemById: (id) =>
personObjectMetadataItem.fields.find((field) => field.id === id),
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -74,7 +75,8 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters,
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
findFieldMetadataItemById: (id) =>
personObjectMetadataItem.fields.find((field) => field.id === id),
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -1,9 +1,7 @@
import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import {
type RecordFilterValueDependencies,
@@ -11,6 +9,7 @@ import {
} from 'twenty-shared/types';
import {
computeRecordGqlOperationFilter,
type FindFieldMetadataItemById,
turnAnyFieldFilterIntoRecordGqlFilter,
} from 'twenty-shared/utils';
@@ -19,7 +18,7 @@ type ComputeContextStoreFiltersProps = {
contextStoreFilters: RecordFilter[];
contextStoreFilterGroups: RecordFilterGroup[];
objectMetadataItem: EnrichedObjectMetadataItem;
flattenedFieldMetadataItems: FieldMetadataItem[];
findFieldMetadataItemById: FindFieldMetadataItemById;
filterValueDependencies: RecordFilterValueDependencies;
contextStoreAnyFieldFilterValue: string;
};
@@ -29,7 +28,7 @@ export const computeContextStoreFilters = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
findFieldMetadataItemById,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
}: ComputeContextStoreFiltersProps) => {
@@ -41,18 +40,12 @@ export const computeContextStoreFilters = ({
fields: objectMetadataItem.fields,
});
const fields = augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters: contextStoreFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
});
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
queryFilter = makeAndFilterVariables([
recordGqlFilterForAnyFieldFilter,
computeRecordGqlOperationFilter({
filterValueDependencies,
fields,
findFieldMetadataItemById,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -81,7 +74,7 @@ export const computeContextStoreFilters = ({
},
computeRecordGqlOperationFilter({
filterValueDependencies,
fields,
findFieldMetadataItemById,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -0,0 +1,16 @@
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const fieldMetadataItemByIdMapSelector = createAtomSelector<
Map<string, FieldMetadataItem>
>({
key: 'fieldMetadataItemByIdMapSelector',
get: ({ get }) =>
new Map(
get(flattenedFieldMetadataItemsSelector).map((field) => [
field.id,
field,
]),
),
});
@@ -1,4 +1,4 @@
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
@@ -7,7 +7,6 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -49,8 +48,8 @@ export const useRecordCalendarQueryDateRangeFilter = (
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -111,11 +110,7 @@ export const useRecordCalendarQueryDateRangeFilter = (
filterValueDependencies,
recordFilters: calendarRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: calendarRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
});
const { recordGqlOperationFilter: anyFieldFilter } =
@@ -22,6 +22,15 @@ const petMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('pet');
const personMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
const findCompanyFieldById = (id: string) =>
companyMockObjectMetadataItem.fields.find((field) => field.id === id);
const findPersonFieldById = (id: string) =>
personMockObjectMetadataItem.fields.find((field) => field.id === id);
const findPetFieldById = (id: string) =>
petMockObjectMetadataItem.fields.find((field) => field.id === id);
const mockFilterValueDependencies: RecordFilterValueDependencies = {
currentWorkspaceMemberId: '32219445-f587-4c40-b2b1-6d3205ed96da',
timeZone: 'Europe/Paris',
@@ -54,7 +63,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [nameFilter],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -107,7 +116,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [nameFilter, employeesFilter],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -187,7 +196,7 @@ describe('should work as expected for the different field types', () => {
addressFilterIsNotEmpty,
],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -651,7 +660,7 @@ describe('should work as expected for the different field types', () => {
phonesFilterIsNotEmpty,
],
recordFilterGroups: [],
fields: personMockObjectMetadataItem.fields,
findFieldMetadataItemById: findPersonFieldById,
});
expect(result).toEqual({
@@ -848,7 +857,7 @@ describe('should work as expected for the different field types', () => {
emailsFilterIsNotEmpty,
],
recordFilterGroups: [],
fields: personMockObjectMetadataItem.fields,
findFieldMetadataItemById: findPersonFieldById,
});
expect(result).toEqual({
@@ -1060,7 +1069,7 @@ describe('should work as expected for the different field types', () => {
dateFilterIsNotEmpty,
],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -1162,7 +1171,7 @@ describe('should work as expected for the different field types', () => {
employeesFilterIsNotEmpty,
],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -1264,7 +1273,7 @@ describe('should work as expected for the different field types', () => {
ARRFilterIsNot,
],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -1341,7 +1350,7 @@ describe('should work as expected for the different field types', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [ARRFilterIn, ARRFilterNotIn],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -1401,7 +1410,7 @@ describe('should work as expected for the different field types', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [selectFilterIs, selectFilterIsNot],
recordFilterGroups: [],
fields: petMockObjectMetadataItem.fields,
findFieldMetadataItemById: findPetFieldById,
});
expect(result).toEqual({
@@ -1474,7 +1483,7 @@ describe('should work as expected for the different field types', () => {
multiSelectFilterDoesNotContain,
],
recordFilterGroups: [],
fields: companyMockObjectMetadataItem.fields,
findFieldMetadataItemById: findCompanyFieldById,
});
expect(result).toEqual({
@@ -1,41 +0,0 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isDefined } from 'twenty-shared/utils';
// Relation-traversal filters reference target fields that live on a
// different object than the one being queried, so they aren't in the
// source object's own field list. The shared GraphQL filter dispatcher
// looks the target up by id in `fieldMetadataItems` and drops the filter
// when it can't find it — this helper merges the resolved target fields
// in from the workspace-wide flat list so callers don't silently lose
// relation-traversal filters.
export const augmentFieldsWithRelationTargets = ({
baseFields,
recordFilters,
allFieldMetadataItems,
}: {
baseFields: FieldMetadataItem[];
recordFilters: Pick<RecordFilter, 'relationTargetFieldMetadataId'>[];
allFieldMetadataItems: FieldMetadataItem[];
}): FieldMetadataItem[] => {
const targetFieldIds = new Set(
recordFilters
.map((filter) => filter.relationTargetFieldMetadataId)
.filter(isDefined),
);
if (targetFieldIds.size === 0) {
return baseFields;
}
const baseFieldIds = new Set(baseFields.map((field) => field.id));
const additionalTargetFields = allFieldMetadataItems.filter(
(field) => targetFieldIds.has(field.id) && !baseFieldIds.has(field.id),
);
if (additionalTargetFields.length === 0) {
return baseFields;
}
return [...baseFields, ...additionalTargetFields];
};
@@ -6,7 +6,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -54,8 +54,8 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const computedFilter = computeContextStoreFilters({
@@ -63,7 +63,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -7,7 +7,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
@@ -89,8 +89,8 @@ export const useRecordIndexLazyFetchRecords = ({
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const findManyRecordsParams = useFindManyRecordIndexTableParams(
@@ -107,7 +107,7 @@ export const useRecordIndexLazyFetchRecords = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -1,12 +1,11 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
@@ -50,16 +49,12 @@ export const useFindManyRecordIndexTableParams = (
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const currentFilters = computeRecordGqlOperationFilter({
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
filterValueDependencies,
@@ -1,12 +1,11 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
import { computeRecordGroupOptionsFilter } from '@/object-record/record-group/utils/computeRecordGroupOptionsFilter';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -39,19 +38,15 @@ export const useRecordIndexGroupCommonQueryVariables = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
});
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -1,5 +1,5 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
@@ -9,7 +9,6 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useAggregateGqlFieldsFromRecordIndexGroupAggregates } from '@/object-record/record-index/hooks/useAggregateGqlFieldsFromRecordIndexGroupAggregates';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject';
@@ -49,19 +48,15 @@ export const useRecordIndexGroupsAggregatesGroupBy = ({
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
});
const { recordAggregateGqlField } =
@@ -1,11 +1,10 @@
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexHasRecordsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexHasRecordsComponentSelector';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
@@ -41,8 +40,8 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const currentRecordFilters = useAtomComponentStateValue(
@@ -64,11 +63,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
objectNameSingular: objectMetadataItem.nameSingular,
variables: {
filter: computeRecordGqlOperationFilter({
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
filterValueDependencies,
@@ -82,7 +77,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
currentRecordFilterGroups,
filterValueDependencies,
currentRecordSorts,
flattenedFieldMetadataItems,
fieldMetadataItemByIdMap,
],
);
@@ -1,4 +1,4 @@
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { transformAggregateRawValueIntoAggregateDisplayValue } from '@/object-record/record-aggregate/utils/transformAggregateRawValueIntoAggregateDisplayValue';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
@@ -7,7 +7,6 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
import { getRecordAggregateDisplayLabel } from '@/object-record/record-index/utils/getRecordndexAggregateDisplayLabel';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
@@ -47,18 +46,14 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
const dateLocale = useAtomStateValue(dateLocaleState);
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const { filterValueDependencies } = useFilterValueDependencies();
const requestFilters = computeRecordGqlOperationFilter({
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
@@ -1,11 +1,10 @@
import { useMemo } from 'react';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
@@ -17,8 +16,8 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const currentRecordFilters = useAtomComponentStateValue(
@@ -40,11 +39,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
objectNameSingular: objectMetadataItem.nameSingular,
variables: {
filter: computeRecordGqlOperationFilter({
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
filterValueDependencies,
@@ -58,7 +53,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
currentRecordFilterGroups,
filterValueDependencies,
currentRecordSorts,
flattenedFieldMetadataItems,
fieldMetadataItemByIdMap,
],
);
@@ -4,7 +4,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -45,8 +45,8 @@ export const useUpdateMultipleRecordsActions = ({
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const graphqlFilter = computeContextStoreFilters({
@@ -54,7 +54,7 @@ export const useUpdateMultipleRecordsActions = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -1,7 +1,6 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import {
computeRecordGqlOperationFilter,
@@ -41,18 +40,14 @@ export const useGraphWidgetQueryCommon = ({
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const widgetRecordFilters = configuration.filter?.recordFilters ?? [];
const gqlOperationFilter = computeRecordGqlOperationFilter({
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: widgetRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
recordFilters: widgetRecordFilters,
recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
@@ -1,11 +1,10 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -28,8 +27,8 @@ export const useGetRecordIndexTotalCount = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const recordGroupsVisibilityFilter = useGetViewGroupsFilters();
@@ -38,11 +37,7 @@ export const useGetRecordIndexTotalCount = () => {
filterValueDependencies,
recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter],
recordFilterGroups: currentRecordFilterGroups,
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter],
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
});
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -213,6 +213,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
relationTargetFieldMetadataId:
viewFilter.relationTargetFieldMetadataId ?? null,
},
}),
);
@@ -1,7 +1,7 @@
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -15,8 +15,8 @@ export const useQueryVariablesFromParentView = ({
}) => {
const { objectMetadataItems } = useObjectMetadataItems();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
);
const contextStoreRecordShowParentView = useAtomComponentStateValue(
@@ -33,7 +33,7 @@ export const useQueryVariablesFromParentView = ({
recordSorts: contextStoreRecordShowParentView?.parentViewSorts ?? [],
objectMetadataItem,
objectMetadataItems,
flattenedFieldMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
filterValueDependencies,
});
@@ -1,12 +1,13 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { type RecordFilterValueDependencies } from 'twenty-shared/types';
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
import {
computeRecordGqlOperationFilter,
type FindFieldMetadataItemById,
} from 'twenty-shared/utils';
export const getQueryVariablesFromFiltersAndSorts = ({
recordFilterGroups,
@@ -14,7 +15,7 @@ export const getQueryVariablesFromFiltersAndSorts = ({
recordSorts,
objectMetadataItem,
objectMetadataItems = [],
flattenedFieldMetadataItems,
findFieldMetadataItemById,
filterValueDependencies,
}: {
recordFilterGroups: RecordFilterGroup[];
@@ -22,15 +23,11 @@ export const getQueryVariablesFromFiltersAndSorts = ({
recordSorts: RecordSort[];
objectMetadataItem: EnrichedObjectMetadataItem;
objectMetadataItems?: EnrichedObjectMetadataItem[];
flattenedFieldMetadataItems: FieldMetadataItem[];
findFieldMetadataItemById: FindFieldMetadataItemById;
filterValueDependencies: RecordFilterValueDependencies;
}) => {
const filter = computeRecordGqlOperationFilter({
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
findFieldMetadataItemById,
filterValueDependencies,
recordFilterGroups,
recordFilters,
@@ -256,37 +256,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
options: field.options as PartialFieldMetadataItemOption[],
}));
// Relation-traversal filters reference target fields on related objects
// that aren't in the source object's field list above. Resolve and add
// them so the shared dispatcher can look them up by id.
const relationTargetFieldsFromFilters = recordFilters
.map((filter) => filter.relationTargetFieldMetadataId)
.filter(isDefined)
.filter((id) => !fields.some((field) => field.id === id))
.map((id) => {
const field = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!field) return null;
return {
id: field.id,
name: field.name,
type: field.type,
label: field.label,
options: field.options as PartialFieldMetadataItemOption[],
};
})
.filter(isDefined);
fields.push(...relationTargetFieldsFromFilters);
const filtersFromView = computeRecordGqlOperationFilter({
recordFilters,
recordFilterGroups: recordFilterGroups,
fields,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
filterValueDependencies: {
timeZone: 'UTC', // TODO: see if we use workspace member timezone here
},
@@ -107,39 +107,12 @@ export class ViewQueryParamsService {
: RecordFilterGroupLogicalOperator.AND,
}));
const filterFieldMetadataIds = recordFilters.flatMap((filter) =>
isDefined(filter.relationTargetFieldMetadataId)
? [filter.fieldMetadataId, filter.relationTargetFieldMetadataId]
: [filter.fieldMetadataId],
);
const fields = filterFieldMetadataIds
.map((fieldMetadataId) => {
const field = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!field) return null;
return {
id: field.id,
name: field.name,
type: field.type,
label: field.label,
options: field.options?.map((opt) => ({
id: opt.id ?? '',
label: opt.label,
value: opt.value,
color: 'color' in opt ? opt.color : undefined,
position: opt.position,
})),
};
})
.filter(isDefined);
const filter = computeRecordGqlOperationFilter({
fields,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
recordFilters,
recordFilterGroups,
filterValueDependencies: { currentWorkspaceMemberId, timeZone },
@@ -5,7 +5,6 @@ import {
RecordFilterGroupLogicalOperator,
RowLevelPermissionPredicateGroupLogicalOperator,
type CompositeFieldSubFieldName,
type PartialFieldMetadataItemOption,
type RecordGqlOperationFilter,
type RowLevelPermissionPredicateValue,
} from 'twenty-shared/types';
@@ -216,26 +215,14 @@ export const buildRowLevelPermissionRecordFilter = ({
predicateGroup.parentRowLevelPermissionPredicateGroupId,
}));
const fieldMetadataItems = predicates
.map((predicate) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: predicate.fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
}),
)
.filter(isDefined)
.map((field) => ({
id: field.id,
name: field.name,
type: field.type,
label: field.label,
options: field.options as PartialFieldMetadataItemOption[],
}));
return computeRecordGqlOperationFilter({
recordFilters,
recordFilterGroups,
fields: fieldMetadataItems,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
filterValueDependencies: {
currentWorkspaceMemberId: workspaceMember?.id,
},
@@ -93,7 +93,6 @@ export class ChartDataQueryService {
}: ExecuteGroupByQueryParams): Promise<GroupByRawResult[]> {
const gqlOperationFilter = convertChartFilterToGqlOperationFilter({
filter,
flatObjectMetadata,
flatFieldMetadataMaps,
userTimezone,
});
@@ -2,7 +2,6 @@ import {
type ChartFilter,
type CompositeFieldSubFieldName,
type FilterableAndTSVectorFieldType,
type PartialFieldMetadataItem,
type RecordFilterGroupLogicalOperator,
type ViewFilterOperand,
} from 'twenty-shared/types';
@@ -18,18 +17,15 @@ import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
type ConvertChartFilterToGqlOperationFilterParams = {
filter: ChartFilter | undefined;
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
userTimezone: string;
};
export const convertChartFilterToGqlOperationFilter = ({
filter,
flatObjectMetadata,
flatFieldMetadataMaps,
userTimezone,
}: ConvertChartFilterToGqlOperationFilterParams): ObjectRecordFilter => {
@@ -44,34 +40,6 @@ export const convertChartFilterToGqlOperationFilter = ({
return {};
}
const fieldIds = flatObjectMetadata.fieldIds ?? [];
const fields: PartialFieldMetadataItem[] = fieldIds
.map((fieldId: string) => {
const field = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(field)) {
return null;
}
return {
id: field.id,
name: field.name,
type: field.type,
label: field.label,
options: field.options?.map((opt) => ({
id: opt.id ?? '',
label: opt.label,
value: opt.value,
color: 'color' in opt ? opt.color : undefined,
position: opt.position,
})),
};
})
.filter(isDefined);
const convertedRecordFilters: Omit<RecordFilter, 'id'>[] = recordFilters.map(
(recordFilter) => {
const field = findFlatEntityByIdInFlatEntityMaps({
@@ -90,6 +58,8 @@ export const convertChartFilterToGqlOperationFilter = ({
subFieldName: (recordFilter.subFieldName ?? undefined) as
| CompositeFieldSubFieldName
| undefined,
relationTargetFieldMetadataId:
recordFilter.relationTargetFieldMetadataId ?? null,
};
},
);
@@ -104,7 +74,11 @@ export const convertChartFilterToGqlOperationFilter = ({
}));
return computeRecordGqlOperationFilter({
fields,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
recordFilters: convertedRecordFilters,
recordFilterGroups: convertedRecordFilterGroups,
filterValueDependencies: {
@@ -1,12 +1,7 @@
import { Injectable } from '@nestjs/common';
import {
type FieldMetadataComplexOption,
type FieldMetadataDefaultOption,
} from 'twenty-shared/types';
import {
computeRecordGqlOperationFilter,
isDefined,
isRecordFilterValueValid,
resolveInput,
} from 'twenty-shared/utils';
@@ -63,38 +58,12 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
const executionContext =
await this.workflowExecutionContextService.getExecutionContext(runInfo);
const { flatObjectMetadata, flatFieldMetadataMaps } =
const { flatFieldMetadataMaps } =
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
workflowActionInput.objectName,
workspaceId,
);
const fields = flatObjectMetadata.fieldIds
.map((fieldId) => {
const field = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!field) {
return null;
}
return {
id: field.id,
name: field.name,
type: field.type,
label: field.label,
// Note: force cast is required until we deprecate the CreateFieldInput and UpdateFieldInput
// type derivation from the FieldMetadataDto
options: field.options as
| (FieldMetadataDefaultOption & { id: string })[]
| (FieldMetadataComplexOption & { id: string })[]
| null,
};
})
.filter(isDefined);
if (workflowActionInput.filter?.recordFilters) {
for (const filter of workflowActionInput.filter.recordFilters) {
if (!isRecordFilterValueValid(filter)) {
@@ -110,7 +79,11 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
workflowActionInput.filter?.recordFilters &&
workflowActionInput.filter?.recordFilterGroups
? computeRecordGqlOperationFilter({
fields,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
recordFilters: workflowActionInput.filter.recordFilters,
recordFilterGroups: workflowActionInput.filter.recordFilterGroups,
filterValueDependencies: {
@@ -0,0 +1,230 @@
import gql from 'graphql-tag';
import request from 'supertest';
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { v4 as uuidv4 } from 'uuid';
const client = request(`http://localhost:${APP_PORT}`);
const TEST_COMPANY_AIRBNB_ID = '20202020-aaaa-4000-8000-000000000001';
const TEST_COMPANY_STRIPE_ID = '20202020-aaaa-4000-8000-000000000002';
const TEST_PERSON_AIRBNB_1_ID = '20202020-bbbb-4000-8000-000000000001';
const TEST_PERSON_AIRBNB_2_ID = '20202020-bbbb-4000-8000-000000000002';
const TEST_PERSON_STRIPE_1_ID = '20202020-bbbb-4000-8000-000000000003';
const SHARED_JOB_TITLE = 'chart-test-relation-traversal';
const ALL_TEST_PERSON_IDS = [
TEST_PERSON_AIRBNB_1_ID,
TEST_PERSON_AIRBNB_2_ID,
TEST_PERSON_STRIPE_1_ID,
];
const ALL_TEST_COMPANY_IDS = [TEST_COMPANY_AIRBNB_ID, TEST_COMPANY_STRIPE_ID];
describe('BarChartData with relation-traversal filter (e2e)', () => {
let personObjectMetadataId: string | null = null;
let personIdFieldMetadataId: string | null = null;
let personJobTitleFieldMetadataId: string | null = null;
let personCompanyFieldMetadataId: string | null = null;
let companyNameFieldMetadataId: string | null = null;
const lookupMetadataIds = async () => {
const objectsResponse = await makeMetadataAPIRequest({
query: gql`
query Objects($filter: ObjectFilter!, $paging: CursorPaging!) {
objects(filter: $filter, paging: $paging) {
edges {
node {
id
nameSingular
fieldsList {
id
name
}
}
}
}
}
`,
variables: { paging: { first: 1000 }, filter: {} },
});
expect(objectsResponse.body.errors).toBeUndefined();
const objects: Array<{
id: string;
nameSingular: string;
fieldsList: Array<{ id: string; name: string }>;
}> = objectsResponse.body.data.objects.edges.map(
(edge: { node: unknown }) => edge.node,
);
const personObject = objects.find((o) => o.nameSingular === 'person');
const companyObject = objects.find((o) => o.nameSingular === 'company');
personObjectMetadataId = personObject?.id ?? null;
personIdFieldMetadataId =
personObject?.fieldsList.find((f) => f.name === 'id')?.id ?? null;
personJobTitleFieldMetadataId =
personObject?.fieldsList.find((f) => f.name === 'jobTitle')?.id ?? null;
personCompanyFieldMetadataId =
personObject?.fieldsList.find((f) => f.name === 'company')?.id ?? null;
companyNameFieldMetadataId =
companyObject?.fieldsList.find((f) => f.name === 'name')?.id ?? null;
if (
!personObjectMetadataId ||
!personIdFieldMetadataId ||
!personJobTitleFieldMetadataId ||
!personCompanyFieldMetadataId ||
!companyNameFieldMetadataId
) {
throw new Error('Failed to resolve required metadata ids for chart test');
}
};
const seedTestRecords = async () => {
await makeGraphqlAPIRequest(
createManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: 'id',
data: [
{ id: TEST_COMPANY_AIRBNB_ID, name: 'AirbnbChartTest' },
{ id: TEST_COMPANY_STRIPE_ID, name: 'StripeChartTest' },
],
upsert: true,
}),
);
await makeGraphqlAPIRequest(
createManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: 'id',
data: [
{
id: TEST_PERSON_AIRBNB_1_ID,
companyId: TEST_COMPANY_AIRBNB_ID,
jobTitle: SHARED_JOB_TITLE,
},
{
id: TEST_PERSON_AIRBNB_2_ID,
companyId: TEST_COMPANY_AIRBNB_ID,
jobTitle: SHARED_JOB_TITLE,
},
{
id: TEST_PERSON_STRIPE_1_ID,
companyId: TEST_COMPANY_STRIPE_ID,
jobTitle: SHARED_JOB_TITLE,
},
],
upsert: true,
}),
);
};
const queryBarChartCount = async (extraRecordFilters: object[] = []) => {
const filterGroupId = uuidv4();
const allRecordFilters = [
{
id: uuidv4(),
type: 'TEXT',
operand: 'CONTAINS',
value: SHARED_JOB_TITLE,
fieldMetadataId: personJobTitleFieldMetadataId,
recordFilterGroupId: filterGroupId,
},
...extraRecordFilters.map((filter) => ({
...filter,
recordFilterGroupId: filterGroupId,
})),
];
const response = await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
query BarChartData($input: BarChartDataInput!) {
barChartData(input: $input) {
data
indexBy
keys
}
}
`,
variables: {
input: {
objectMetadataId: personObjectMetadataId,
configuration: {
configurationType: 'BAR_CHART',
layout: 'VERTICAL',
aggregateFieldMetadataId: personIdFieldMetadataId,
aggregateOperation: 'COUNT',
primaryAxisGroupByFieldMetadataId: personJobTitleFieldMetadataId,
primaryAxisOrderBy: 'VALUE_DESC',
filter: {
recordFilters: allRecordFilters,
recordFilterGroups: [
{ id: filterGroupId, logicalOperator: 'AND' },
],
},
},
},
},
});
expect(response.body.errors).toBeUndefined();
const data: Array<Record<string, string | number>> =
response.body.data.barChartData.data;
const row = data.find((entry) => entry.jobTitle === SHARED_JOB_TITLE);
return typeof row?.id === 'number' ? row.id : 0;
};
beforeAll(async () => {
await lookupMetadataIds();
await seedTestRecords();
});
afterAll(async () => {
await makeGraphqlAPIRequest(
deleteManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: 'id',
filter: { id: { in: ALL_TEST_PERSON_IDS } },
}),
);
await makeGraphqlAPIRequest(
deleteManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: 'id',
filter: { id: { in: ALL_TEST_COMPANY_IDS } },
}),
);
});
it('should count all 3 test people without a relation-traversal filter', async () => {
const count = await queryBarChartCount();
expect(count).toBe(3);
});
it('should apply a one-hop relation-traversal filter and only count Airbnb people', async () => {
const count = await queryBarChartCount([
{
id: uuidv4(),
type: 'TEXT',
operand: 'CONTAINS',
value: 'AirbnbChartTest',
fieldMetadataId: personCompanyFieldMetadataId,
relationTargetFieldMetadataId: companyNameFieldMetadataId,
},
]);
expect(count).toBe(2);
});
});
@@ -0,0 +1,348 @@
import gql from 'graphql-tag';
import request from 'supertest';
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import {
destroyWorkflowRun,
runWorkflowVersion,
waitForWorkflowCompletion,
} from 'test/integration/graphql/suites/workflow/utils/workflow-run-test.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { v4 as uuidv4 } from 'uuid';
const client = request(`http://localhost:${APP_PORT}`);
const TEST_COMPANY_AIRBNB_ID = '20202020-eeee-4000-8000-000000000001';
const TEST_COMPANY_STRIPE_ID = '20202020-eeee-4000-8000-000000000002';
const TEST_PERSON_AIRBNB_1_ID = '20202020-ffff-4000-8000-000000000001';
const TEST_PERSON_AIRBNB_2_ID = '20202020-ffff-4000-8000-000000000002';
const TEST_PERSON_STRIPE_1_ID = '20202020-ffff-4000-8000-000000000003';
const ALL_TEST_PERSON_IDS = [
TEST_PERSON_AIRBNB_1_ID,
TEST_PERSON_AIRBNB_2_ID,
TEST_PERSON_STRIPE_1_ID,
];
const ALL_TEST_COMPANY_IDS = [TEST_COMPANY_AIRBNB_ID, TEST_COMPANY_STRIPE_ID];
describe('FindRecords workflow action with relation-traversal filter (e2e)', () => {
let createdWorkflowId: string | null = null;
let createdWorkflowVersionId: string | null = null;
let findRecordsStepId: string | null = null;
let createdWorkflowRunId: string | null = null;
let personCompanyFieldMetadataId: string | null = null;
let companyNameFieldMetadataId: string | null = null;
const lookupFieldMetadataIds = async () => {
const objectsResponse = await makeMetadataAPIRequest({
query: gql`
query Objects($filter: ObjectFilter!, $paging: CursorPaging!) {
objects(filter: $filter, paging: $paging) {
edges {
node {
nameSingular
fieldsList {
id
name
}
}
}
}
}
`,
variables: { paging: { first: 1000 }, filter: {} },
});
expect(objectsResponse.body.errors).toBeUndefined();
const objects: Array<{
nameSingular: string;
fieldsList: Array<{ id: string; name: string }>;
}> = objectsResponse.body.data.objects.edges.map(
(edge: { node: unknown }) => edge.node,
);
const personObject = objects.find((o) => o.nameSingular === 'person');
const companyObject = objects.find((o) => o.nameSingular === 'company');
personCompanyFieldMetadataId =
personObject?.fieldsList.find((f) => f.name === 'company')?.id ?? null;
companyNameFieldMetadataId =
companyObject?.fieldsList.find((f) => f.name === 'name')?.id ?? null;
if (!personCompanyFieldMetadataId || !companyNameFieldMetadataId) {
throw new Error(
`Could not resolve required field metadata ids — person.company=${personCompanyFieldMetadataId}, company.name=${companyNameFieldMetadataId}`,
);
}
};
const seedTestRecords = async () => {
await makeGraphqlAPIRequest(
createManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: 'id',
data: [
{ id: TEST_COMPANY_AIRBNB_ID, name: 'AirbnbWorkflowTest' },
{ id: TEST_COMPANY_STRIPE_ID, name: 'StripeWorkflowTest' },
],
upsert: true,
}),
);
await makeGraphqlAPIRequest(
createManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: 'id',
data: [
{
id: TEST_PERSON_AIRBNB_1_ID,
companyId: TEST_COMPANY_AIRBNB_ID,
jobTitle: 'workflow-test-airbnb-1',
},
{
id: TEST_PERSON_AIRBNB_2_ID,
companyId: TEST_COMPANY_AIRBNB_ID,
jobTitle: 'workflow-test-airbnb-2',
},
{
id: TEST_PERSON_STRIPE_1_ID,
companyId: TEST_COMPANY_STRIPE_ID,
jobTitle: 'workflow-test-stripe-1',
},
],
upsert: true,
}),
);
};
const buildWorkflow = async () => {
const createWorkflowResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation CreateWorkflow {
createWorkflow(data: { name: "Relation Traversal Find Records Test" }) {
id
}
}
`,
});
expect(createWorkflowResponse.body.errors).toBeUndefined();
createdWorkflowId = createWorkflowResponse.body.data.createWorkflow.id;
const getWorkflowResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
query GetWorkflow($id: UUID!) {
workflow(filter: { id: { eq: $id } }) {
id
versions { edges { node { id } } }
}
}
`,
variables: { id: createdWorkflowId },
});
createdWorkflowVersionId =
getWorkflowResponse.body.data.workflow.versions.edges[0].node.id;
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation UpdateWorkflowVersion($id: UUID!, $data: WorkflowVersionUpdateInput!) {
updateWorkflowVersion(id: $id, data: $data) { id }
}
`,
variables: {
id: createdWorkflowVersionId,
data: {
trigger: {
name: 'Manual Trigger',
type: 'MANUAL',
settings: { outputSchema: {} },
nextStepIds: [],
position: { x: 0, y: 0 },
},
},
},
});
const createStepResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation CreateWorkflowVersionStep($input: CreateWorkflowVersionStepInput!) {
createWorkflowVersionStep(input: $input) { stepsDiff }
}
`,
variables: {
input: {
workflowVersionId: createdWorkflowVersionId,
stepType: 'FIND_RECORDS',
parentStepId: 'trigger',
position: { x: 200, y: 0 },
},
},
});
expect(createStepResponse.body.errors).toBeUndefined();
const getVersionResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
query GetWorkflowVersion($id: UUID!) {
workflowVersion(filter: { id: { eq: $id } }) { steps }
}
`,
variables: { id: createdWorkflowVersionId },
});
const steps = getVersionResponse.body.data.workflowVersion.steps;
const findRecordsStep = steps.find(
(step: { type: string }) => step.type === 'FIND_RECORDS',
);
expect(findRecordsStep).toBeDefined();
findRecordsStepId = findRecordsStep.id;
const filterGroupId = uuidv4();
const filterId = uuidv4();
const updateStepResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation UpdateWorkflowVersionStep($input: UpdateWorkflowVersionStepInput!) {
updateWorkflowVersionStep(input: $input) { id }
}
`,
variables: {
input: {
workflowVersionId: createdWorkflowVersionId,
step: {
...findRecordsStep,
settings: {
...findRecordsStep.settings,
input: {
...findRecordsStep.settings.input,
objectName: 'person',
limit: 25,
filter: {
recordFilters: [
{
id: filterId,
type: 'TEXT',
label: 'Company → Name',
value: 'AirbnbWorkflowTest',
operand: 'CONTAINS',
displayValue: 'AirbnbWorkflowTest',
fieldMetadataId: personCompanyFieldMetadataId,
relationTargetFieldMetadataId:
companyNameFieldMetadataId,
recordFilterGroupId: filterGroupId,
},
],
recordFilterGroups: [
{ id: filterGroupId, logicalOperator: 'AND' },
],
},
},
},
},
},
},
});
expect(updateStepResponse.body.errors).toBeUndefined();
const activateResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation ActivateWorkflowVersion($workflowVersionId: UUID!) {
activateWorkflowVersion(workflowVersionId: $workflowVersionId)
}
`,
variables: { workflowVersionId: createdWorkflowVersionId },
});
expect(activateResponse.body.errors).toBeUndefined();
};
beforeAll(async () => {
await lookupFieldMetadataIds();
await seedTestRecords();
await buildWorkflow();
});
afterAll(async () => {
if (createdWorkflowRunId) {
await destroyWorkflowRun(createdWorkflowRunId);
}
if (createdWorkflowId) {
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `mutation DestroyWorkflow($id: ID!) { destroyWorkflow(id: $id) { id } }`,
variables: { id: createdWorkflowId },
});
}
await makeGraphqlAPIRequest(
deleteManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: 'id',
filter: { id: { in: ALL_TEST_PERSON_IDS } },
}),
);
await makeGraphqlAPIRequest(
deleteManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: 'id',
filter: { id: { in: ALL_TEST_COMPANY_IDS } },
}),
);
});
it('should apply a one-hop relation-traversal filter and return only matching records', async () => {
const workflowRunId = await runWorkflowVersion({
workflowVersionId: createdWorkflowVersionId!,
});
createdWorkflowRunId = workflowRunId;
const workflowRun = await waitForWorkflowCompletion(workflowRunId);
expect(workflowRun?.status).toBe('COMPLETED');
expect(workflowRun?.state?.stepInfos?.[findRecordsStepId!]?.status).toBe(
'SUCCESS',
);
const result = workflowRun?.state?.stepInfos?.[findRecordsStepId!]
?.result as
| { all?: Array<{ id: string }>; totalCount?: number }
| undefined;
const returnedIds = (result?.all ?? []).map((record) => record.id);
expect(returnedIds).toContain(TEST_PERSON_AIRBNB_1_ID);
expect(returnedIds).toContain(TEST_PERSON_AIRBNB_2_ID);
expect(returnedIds).not.toContain(TEST_PERSON_STRIPE_1_ID);
});
});
@@ -8,6 +8,7 @@ export type ChartRecordFilter = {
type?: string;
recordFilterGroupId?: string | null;
subFieldName?: string | null;
relationTargetFieldMetadataId?: SerializedRelation | null;
};
export type ChartRecordFilterGroup = {
@@ -27,7 +27,8 @@ describe('computeRecordGqlOperationFilter', () => {
];
const filter = computeRecordGqlOperationFilter({
fields: [companyIdField],
findFieldMetadataItemById: (id) =>
id === companyIdField.id ? companyIdField : undefined,
recordFilters,
recordFilterGroups: [],
filterValueDependencies: {
@@ -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',
@@ -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,
}),
)
@@ -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';