Compare commits

...
Author SHA1 Message Date
Félix Malfait ca0f04923c chore(filters): prettier format 2026-05-18 15:58:54 +02:00
Félix Malfait be3b3c95f1 refactor(filters): selector returns Map; collapse server resolvers to one-liners
Selector pivot:
- fieldMetadataItemByIdMapSelector returns Map<string, FieldMetadataItem>
  (conventional shape — selector returns data, not behavior).
- Callers pull the map and inline `(id) => fieldMetadataItemByIdMap.get(id)`
  when passing to the dispatcher. No naming gymnastics, no
  selector-holding-a-function pattern.

Server resolver simplifications:
- find-records.workflow-action, build-row-level-permission-record-filter,
  view-query-params, common-group-by-query-runner, convert-chart-filter:
  drop the inline `{id, name, type, label, options}` rebuild — the
  dispatcher's FieldShared only reads id/name/type/label and
  findFlatEntityByIdInFlatEntityMaps already returns a superset.
  Each resolver collapses to `(id) => findFlatEntityByIdInFlatEntityMaps({...})`.
2026-05-18 15:55:10 +02:00
Félix Malfait ff2f86848c refactor(filters): introduce findFieldMetadataItemByIdSelector and drop createFindFieldMetadataItemById helper
The convenience helper was a sign that callers were each reindexing the
same workspace-wide flat field metadata array into the same lookup map.
The fix is upstream: derive the lookup once in a Jotai selector and
remove the helper.

- New `findFieldMetadataItemByIdSelector` (twenty-front) builds the id
  index once from `flattenedFieldMetadataItemsSelector` and returns a
  `FindFieldMetadataItemById` callback.
- All 10 Jotai-using callers (record-table SSE, record-table empty,
  table-column-footer, record-index group-by, group common variables,
  table params, graph widget, calendar date range, total count) pull
  the callback from the selector instead of indexing themselves.
- Two pure utilities (`computeContextStoreFilters`,
  `getQueryVariablesFromFiltersAndSorts`) now take
  `findFieldMetadataItemById` directly; their 6 callers pass it from
  the selector (or `store.get` for the non-React path).
- `useFindManyRecordsSelectedInContextStore` also routes the
  `deletedAt` lookup through the resolver instead of re-flattening.
- `turnAnyFieldFilterIntoRecordGqlFilter` (twenty-shared) and
  `buildRowLevelPermissionRecordFilter` (twenty-server) inline the
  resolver — the server one collapses to a one-liner against
  `flatFieldMetadataMaps`, dropping the per-predicate field-id
  remapping it used to do.
- Dispatcher tests inline `Map`-backed resolvers.
- `createFindFieldMetadataItemById` and its exports are deleted.
2026-05-18 15:13:29 +02:00
Félix Malfait f1ce22db97 fix(test): use empty ObjectFilter + filter-in-memory for metadata id lookup
ObjectFilter / FieldFilter don't expose nameSingular / name as filterable
columns, so the previous targeted queries returned a 400 ("Field
\"nameSingular\" is not defined by type \"ObjectFilter\""). Use the
established pattern from successful-view-filter-relation-traversal:
fetch all objects with fieldsList inlined, then find by name in memory.
2026-05-18 14:28:28 +02:00
Félix Malfait 90ea800f16 feat(filters): extend chart filter type for relation traversal + add regression-guarding integration tests
Charts couldn't model one-hop relation-traversal filters because
ChartRecordFilter lacked relationTargetFieldMetadataId. The frontend
chart-filter UI reuses the shared AdvancedFilterSidePanelContainer
(which already produces relation-traversal filters), and the page-layout
widget configuration is stored as JSONB — so adding the field on the
shared type is the only change needed to plumb traversal end-to-end.

Tests added (both regression-guard the silently-dropped-filter bug):
- find-records-relation-traversal-workflow.integration-spec.ts:
  runs a real workflow with a FIND_RECORDS step configured with a
  one-hop traversal filter (Person where Company → Name CONTAINS
  "AirbnbWorkflowTest"), asserts only matching people come back.
- chart-data-relation-traversal-filter.integration-spec.ts: queries
  barChartData with and without a relation-traversal filter, asserts
  the aggregate count is 2 (Airbnb-only) when filtered, 3 unfiltered.
  Without the dispatcher fix the filtered count would also be 3.
2026-05-18 14:14:15 +02:00
Félix Malfait ee4fe62a13 fix(filters): drop chart-util's strip of non-existent relationTargetFieldMetadataId + regenerate barrel
ChartRecordFilter does not currently include relationTargetFieldMetadataId
(relation traversal isn't supported in chart filters yet), so reading
recordFilter.relationTargetFieldMetadataId in the chart converter was a
typecheck error in the first place.
2026-05-18 13:58:25 +02:00
Félix Malfait 95b64f2eda chore(filters): prettier format 2026-05-18 13:43:50 +02:00
Félix Malfait 11e408a3da refactor(filters): make dispatcher own field resolution to prevent dropped relation traversals
Replace the dispatcher's `fields` parameter with a `findFieldMetadataItemById`
resolver callback. Both source-field and relation-target-field lookups now go
through the same resolver, so callers no longer need to remember to pre-augment
their `fields` list with relation targets — if they pass a workspace-wide
resolver (or any resolver that can find the target), traversal filters
just work.

This eliminates a whack-a-mole pattern that had silently caused multiple bugs:
the previous API took a `fields: FieldShared[]` array, and any caller that
forgot to include relation-target field ids would silently drop relation
traversal filters at the GraphQL-compute step (the dispatcher couldn't
resolve the target, so it dropped the whole filter). 16+ call sites needed
to know this; several didn't, including the workflow Search Records action
in the previous commit.

Changes:
- twenty-shared: dispatcher (`computeRecordGqlOperationFilter`,
  `turnRecordFilterGroupIntoGqlOperationFilter`,
  `turnRecordFilterIntoRecordGqlOperationFilter`,
  `turnAnyFieldFilterIntoRecordGqlFilter`) now takes
  `findFieldMetadataItemById: (id) => FieldShared | undefined`.
  Adds `createFindFieldMetadataItemById(fields)` convenience constructor
  for callers with an array on hand.
- twenty-front: all 10 call sites pass
  `createFindFieldMetadataItemById(flattenedFieldMetadataItems)` (workspace-
  wide resolver from the existing selector). Deletes the now-obsolete
  `augmentFieldsWithRelationTargets` helper.
- twenty-server: all 5 call sites use the resolver pattern with
  `findFlatEntityByIdInFlatEntityMaps` against `flatFieldMetadataMaps`.
  Reverts the workflow whack-a-mole code from the previous commit
  (the proper fix supersedes it) and drops the stripping of
  `relationTargetFieldMetadataId` in the chart-data filter converter.
2026-05-18 13:37:59 +02:00
Félix Malfait eaccc68ca1 fix(filters): persist relationTargetFieldMetadataId on save-as-new-view + workflow find-records
Two relation-traversal bugs sharing the same shape:

1. Saving an advanced filter as a new view dropped the relation target.
   useCreateViewFromCurrentView built the create-filter input without
   relationTargetFieldMetadataId, so the saved view's filter was missing
   the traversal — on reload it appeared as "Company contains 'air'"
   instead of "Company → Name contains 'air'".

2. Workflow Search Records action silently dropped configured one-hop
   traversals. The fields list was built from flatObjectMetadata.fieldIds
   only, so the shared dispatcher couldn't resolve the target field on
   the related object and dropped the filter.

For (2), collect relationTargetFieldMetadataId from the input record
filters and union them into the fieldIds list before resolving against
flatFieldMetadataMaps — mirroring the fix already applied to other
filter-compute call sites.
2026-05-18 11:57:19 +02:00
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';