Compare commits

..
Author SHA1 Message Date
Charles Bochet 0b7b33fe18 fix(server): backport relationTargetFieldMetadataId column-add to 2.4 and 2.5 fast instance
Backports the column-add to two new fast instance commands (2.4 at
timestamp 1747234400000 and 2.5 at 1747234500000) so users at v2.3.x or
v2.4.x baselines can upgrade past v2.5 without hitting the 2.5 workspace
command NormalizeCompositeFieldDefaults failing on
`column ViewFilterEntity.relationTargetFieldMetadataId does not exist`.

Same fix as main #20721, scoped to v2.5.x. ADD COLUMN IF NOT EXISTS keeps
both idempotent against DBs that already received the column via the 2.3
backport or the 2.6 add.
2026-05-19 16:02:30 +02:00
Charles Bochet f1125d0894 chore: revert version constants to 2.5.0 for v2.5.3 patch release
The v2.5.x patch tags (v2.5.0, v2.5.1, v2.5.2) were cut from main *after* #20585 bumped TWENTY_CURRENT_VERSION to 2.6.0, so all three binaries report "2.6.0" in the admin-panel inferred-version view despite being tagged 2.5.x.

Restore the version constants to the values they had at v2.5.0 so v2.5.3 reports the correct minor version. Same shape as a pure revert of #20585.
2026-05-18 11:36:42 +02:00
48 changed files with 475 additions and 887 deletions
@@ -9,7 +9,6 @@ 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';
@@ -90,8 +89,8 @@ export const buildHeadlessCommandContextApi = ({
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
: systemTimeZone;
const fieldMetadataItemByIdMap = store.get(
fieldMetadataItemByIdMapSelector.atom,
const flattenedFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
);
const graphqlFilter = isDefined(objectMetadataItem)
@@ -100,7 +99,7 @@ export const buildHeadlessCommandContextApi = ({
contextStoreFilters: filters,
contextStoreFilterGroups: filterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
flattenedFieldMetadataItems,
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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
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,22 +50,29 @@ export const useFindManyRecordsSelectedInContextStore = ({
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const allFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
);
const isSoftDeleteFilterActive = contextStoreFilters.some(
(filter) =>
fieldMetadataItemByIdMap.get(filter.fieldMetadataId)?.name ===
'deletedAt' && filter.operand === RecordFilterOperand.IS_NOT_EMPTY,
);
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 queryFilter = computeContextStoreFilters({
contextStoreTargetedRecordsRule,
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
flattenedFieldMetadataItems: allFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -29,8 +29,7 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters: [],
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
findFieldMetadataItemById: (id) =>
personObjectMetadataItem.fields.find((field) => field.id === id),
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -75,8 +74,7 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters,
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
findFieldMetadataItemById: (id) =>
personObjectMetadataItem.fields.find((field) => field.id === id),
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -1,7 +1,9 @@
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,
@@ -9,7 +11,6 @@ import {
} from 'twenty-shared/types';
import {
computeRecordGqlOperationFilter,
type FindFieldMetadataItemById,
turnAnyFieldFilterIntoRecordGqlFilter,
} from 'twenty-shared/utils';
@@ -18,7 +19,7 @@ type ComputeContextStoreFiltersProps = {
contextStoreFilters: RecordFilter[];
contextStoreFilterGroups: RecordFilterGroup[];
objectMetadataItem: EnrichedObjectMetadataItem;
findFieldMetadataItemById: FindFieldMetadataItemById;
flattenedFieldMetadataItems: FieldMetadataItem[];
filterValueDependencies: RecordFilterValueDependencies;
contextStoreAnyFieldFilterValue: string;
};
@@ -28,7 +29,7 @@ export const computeContextStoreFilters = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById,
flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
}: ComputeContextStoreFiltersProps) => {
@@ -40,12 +41,18 @@ 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,
findFieldMetadataItemById,
fields,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -74,7 +81,7 @@ export const computeContextStoreFilters = ({
},
computeRecordGqlOperationFilter({
filterValueDependencies,
findFieldMetadataItemById,
fields,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -1,16 +0,0 @@
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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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,6 +7,7 @@ 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';
@@ -48,8 +49,8 @@ export const useRecordCalendarQueryDateRangeFilter = (
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -110,7 +111,11 @@ export const useRecordCalendarQueryDateRangeFilter = (
filterValueDependencies,
recordFilters: calendarRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: calendarRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
});
const { recordGqlOperationFilter: anyFieldFilter } =
@@ -22,15 +22,6 @@ 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',
@@ -63,7 +54,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [nameFilter],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -116,7 +107,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [nameFilter, employeesFilter],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -196,7 +187,7 @@ describe('should work as expected for the different field types', () => {
addressFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -660,7 +651,7 @@ describe('should work as expected for the different field types', () => {
phonesFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findPersonFieldById,
fields: personMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -857,7 +848,7 @@ describe('should work as expected for the different field types', () => {
emailsFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findPersonFieldById,
fields: personMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -1069,7 +1060,7 @@ describe('should work as expected for the different field types', () => {
dateFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -1171,7 +1162,7 @@ describe('should work as expected for the different field types', () => {
employeesFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -1273,7 +1264,7 @@ describe('should work as expected for the different field types', () => {
ARRFilterIsNot,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -1350,7 +1341,7 @@ describe('should work as expected for the different field types', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [ARRFilterIn, ARRFilterNotIn],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -1410,7 +1401,7 @@ describe('should work as expected for the different field types', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [selectFilterIs, selectFilterIsNot],
recordFilterGroups: [],
findFieldMetadataItemById: findPetFieldById,
fields: petMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -1483,7 +1474,7 @@ describe('should work as expected for the different field types', () => {
multiSelectFilterDoesNotContain,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fields: companyMockObjectMetadataItem.fields,
});
expect(result).toEqual({
@@ -0,0 +1,41 @@
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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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 fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const computedFilter = computeContextStoreFilters({
@@ -63,7 +63,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
flattenedFieldMetadataItems,
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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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 fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const findManyRecordsParams = useFindManyRecordIndexTableParams(
@@ -107,7 +107,7 @@ export const useRecordIndexLazyFetchRecords = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -1,11 +1,12 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -49,12 +50,16 @@ export const useFindManyRecordIndexTableParams = (
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentFilters = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
filterValueDependencies,
@@ -1,11 +1,12 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -38,15 +39,19 @@ export const useRecordIndexGroupCommonQueryVariables = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
});
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -1,5 +1,5 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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,6 +9,7 @@ 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';
@@ -48,15 +49,19 @@ export const useRecordIndexGroupsAggregatesGroupBy = ({
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
});
const { recordAggregateGqlField } =
@@ -1,10 +1,11 @@
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -40,8 +41,8 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentRecordFilters = useAtomComponentStateValue(
@@ -63,7 +64,11 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
objectNameSingular: objectMetadataItem.nameSingular,
variables: {
filter: computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
filterValueDependencies,
@@ -77,7 +82,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
currentRecordFilterGroups,
filterValueDependencies,
currentRecordSorts,
fieldMetadataItemByIdMap,
flattenedFieldMetadataItems,
],
);
@@ -1,4 +1,4 @@
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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,6 +7,7 @@ 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';
@@ -46,14 +47,18 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
const dateLocale = useAtomStateValue(dateLocaleState);
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const { filterValueDependencies } = useFilterValueDependencies();
const requestFilters = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
filterValueDependencies,
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
@@ -1,10 +1,11 @@
import { useMemo } from 'react';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -16,8 +17,8 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentRecordFilters = useAtomComponentStateValue(
@@ -39,7 +40,11 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
objectNameSingular: objectMetadataItem.nameSingular,
variables: {
filter: computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
filterValueDependencies,
@@ -53,7 +58,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
currentRecordFilterGroups,
filterValueDependencies,
currentRecordSorts,
fieldMetadataItemByIdMap,
flattenedFieldMetadataItems,
],
);
@@ -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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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 fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const graphqlFilter = computeContextStoreFilters({
@@ -54,7 +54,7 @@ export const useUpdateMultipleRecordsActions = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -1,6 +1,7 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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,
@@ -40,14 +41,18 @@ export const useGraphWidgetQueryCommon = ({
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const widgetRecordFilters = configuration.filter?.recordFilters ?? [];
const gqlOperationFilter = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: widgetRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
filterValueDependencies,
recordFilters: widgetRecordFilters,
recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
@@ -1,9 +1,6 @@
import { useContext, useId, useState } from 'react';
import { createPortal } from 'react-dom';
import { styled } from '@linaria/react';
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/display';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { isDefined, formatToShortNumber } from 'twenty-shared/utils';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
@@ -13,13 +10,10 @@ import {
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { isDefined, formatToShortNumber } from 'twenty-shared/utils';
import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/formatNumber';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const StyledCurrencyIconContainer = styled.span`
align-items: center;
display: flex;
`;
import { ThemeContext } from 'twenty-ui/theme-constants';
type CurrencyDisplayProps = {
currencyValue: FieldCurrencyValue | null | undefined;
@@ -63,7 +57,7 @@ export const CurrencyDisplay = ({
<EllipsisDisplay>
{shouldShowCurrencyTooltip && (
<>
<StyledCurrencyIconContainer
<span
id={tooltipAnchorId}
onMouseEnter={() => setShouldRenderTooltip(true)}
onMouseLeave={() => setShouldRenderTooltip(false)}
@@ -73,7 +67,7 @@ export const CurrencyDisplay = ({
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledCurrencyIconContainer>{' '}
</span>{' '}
</>
)}
{amountToDisplay !== null
@@ -1,10 +1,11 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -27,8 +28,8 @@ export const useGetRecordIndexTotalCount = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const recordGroupsVisibilityFilter = useGetViewGroupsFilters();
@@ -37,7 +38,11 @@ export const useGetRecordIndexTotalCount = () => {
filterValueDependencies,
recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter],
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter],
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
});
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -213,8 +213,6 @@ 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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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 fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const contextStoreRecordShowParentView = useAtomComponentStateValue(
@@ -33,7 +33,7 @@ export const useQueryVariablesFromParentView = ({
recordSorts: contextStoreRecordShowParentView?.parentViewSorts ?? [],
objectMetadataItem,
objectMetadataItems,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
flattenedFieldMetadataItems,
filterValueDependencies,
});
@@ -1,13 +1,12 @@
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,
type FindFieldMetadataItemById,
} from 'twenty-shared/utils';
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
export const getQueryVariablesFromFiltersAndSorts = ({
recordFilterGroups,
@@ -15,7 +14,7 @@ export const getQueryVariablesFromFiltersAndSorts = ({
recordSorts,
objectMetadataItem,
objectMetadataItems = [],
findFieldMetadataItemById,
flattenedFieldMetadataItems,
filterValueDependencies,
}: {
recordFilterGroups: RecordFilterGroup[];
@@ -23,11 +22,15 @@ export const getQueryVariablesFromFiltersAndSorts = ({
recordSorts: RecordSort[];
objectMetadataItem: EnrichedObjectMetadataItem;
objectMetadataItems?: EnrichedObjectMetadataItem[];
findFieldMetadataItemById: FindFieldMetadataItemById;
flattenedFieldMetadataItems: FieldMetadataItem[];
filterValueDependencies: RecordFilterValueDependencies;
}) => {
const filter = computeRecordGqlOperationFilter({
findFieldMetadataItemById,
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
filterValueDependencies,
recordFilterGroups,
recordFilters,
@@ -0,0 +1,21 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.4.0', 1747234400000)
export class AddRelationTargetFieldMetadataIdToViewFilterEarly2_4FastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."viewFilter" ADD COLUMN IF NOT EXISTS "relationTargetFieldMetadataId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."viewFilter" DROP COLUMN IF EXISTS "relationTargetFieldMetadataId"`,
);
}
}
@@ -0,0 +1,21 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.5.0', 1747234500000)
export class AddRelationTargetFieldMetadataIdToViewFilterEarly2_5FastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."viewFilter" ADD COLUMN IF NOT EXISTS "relationTargetFieldMetadataId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."viewFilter" DROP COLUMN IF EXISTS "relationTargetFieldMetadataId"`,
);
}
}
@@ -22,6 +22,7 @@ import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
import { AddSubFieldNameToViewSortEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort';
import { AddRelationTargetFieldMetadataIdToViewFilterEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234300000-add-relation-target-field-metadata-id-to-view-filter';
import { AddRelationTargetFieldMetadataIdToViewFilterEarly2_5FastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1747234500000-add-relation-target-field-metadata-id-to-view-filter';
import { AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777308014234-add-upgrade-migration-workspace-id-index';
import { AddDeletedAtToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777682000000-add-deleted-at-to-agent-chat-thread';
import { ConnectionProviderSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777896012579-connection-provider-syncable-entity';
@@ -31,6 +32,7 @@ import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/
import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier';
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
import { AddMetadataToBillingPriceFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1777100000000-add-metadata-to-billing-price';
import { AddRelationTargetFieldMetadataIdToViewFilterEarly2_4FastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1747234400000-add-relation-target-field-metadata-id-to-view-filter';
import { AddEmailGroupChannelTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1778256809018-add-email-group-channel-type';
import { AddApplicationIdToPublicDomainFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain';
import { AddIsInternalMessagesImportEnabledFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1778525104406-add-is-internal-messages-import-enabled';
@@ -63,11 +65,13 @@ export const INSTANCE_COMMANDS = [
AddConditionalAvailabilityExpressionToPageLayoutWidgetFastInstanceCommand,
AddSubFieldNameToViewSortEarlyFastInstanceCommand,
AddRelationTargetFieldMetadataIdToViewFilterEarlyFastInstanceCommand,
AddRelationTargetFieldMetadataIdToViewFilterEarly2_5FastInstanceCommand,
AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand,
AddIsPreInstalledToApplicationRegistrationFastInstanceCommand,
AddProviderExecutedToAgentMessagePartFastInstanceCommand,
BackfillPageLayoutWidgetPositionSlowInstanceCommand,
AddMetadataToBillingPriceFastInstanceCommand,
AddRelationTargetFieldMetadataIdToViewFilterEarly2_4FastInstanceCommand,
AddCacheTokensToAgentChatThreadFastInstanceCommand,
AddLogoToApplicationFastInstanceCommand,
AddDeletedAtToAgentChatThreadFastInstanceCommand,
@@ -256,14 +256,37 @@ 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,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
fields,
filterValueDependencies: {
timeZone: 'UTC', // TODO: see if we use workspace member timezone here
},
@@ -259,18 +259,7 @@ describe('computeSchemaComponents', () => {
"properties": {
"additionalPhones": {
"items": {
"properties": {
"callingCode": {
"type": "string",
},
"countryCode": {
"type": "string",
},
"number": {
"type": "string",
},
},
"type": "object",
"type": "string",
},
"type": "array",
},
@@ -502,18 +491,7 @@ describe('computeSchemaComponents', () => {
"properties": {
"additionalPhones": {
"items": {
"properties": {
"callingCode": {
"type": "string",
},
"countryCode": {
"type": "string",
},
"number": {
"type": "string",
},
},
"type": "object",
"type": "string",
},
"type": "array",
},
@@ -774,18 +752,7 @@ describe('computeSchemaComponents', () => {
"properties": {
"additionalPhones": {
"items": {
"properties": {
"callingCode": {
"type": "string",
},
"countryCode": {
"type": "string",
},
"number": {
"type": "string",
},
},
"type": "object",
"type": "string",
},
"type": "array",
},
@@ -7,4 +7,4 @@
* |___/
*/
export const TWENTY_CURRENT_VERSION = '2.6.0' as const;
export const TWENTY_CURRENT_VERSION = '2.5.0' as const;
@@ -7,6 +7,4 @@
* |___/
*/
export const TWENTY_NEXT_VERSIONS = [
'2.7.0',
] as const;
export const TWENTY_NEXT_VERSIONS = ['2.6.0'] as const;
@@ -16,5 +16,4 @@ export const TWENTY_PREVIOUS_VERSIONS = [
'2.2.0',
'2.3.0',
'2.4.0',
'2.5.0',
] as const;
@@ -107,12 +107,39 @@ export class ViewQueryParamsService {
: RecordFilterGroupLogicalOperator.AND,
}));
const filter = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
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,
recordFilters,
recordFilterGroups,
filterValueDependencies: { currentWorkspaceMemberId, timeZone },
@@ -5,6 +5,7 @@ import {
RecordFilterGroupLogicalOperator,
RowLevelPermissionPredicateGroupLogicalOperator,
type CompositeFieldSubFieldName,
type PartialFieldMetadataItemOption,
type RecordGqlOperationFilter,
type RowLevelPermissionPredicateValue,
} from 'twenty-shared/types';
@@ -215,14 +216,26 @@ 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,
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
fields: fieldMetadataItems,
filterValueDependencies: {
currentWorkspaceMemberId: workspaceMember?.id,
},
@@ -308,18 +308,7 @@ export const convertObjectMetadataToSchemaProperties = ({
additionalPhones: {
type: 'array',
items: {
type: 'object',
properties: {
number: {
type: 'string',
},
countryCode: {
type: 'string',
},
callingCode: {
type: 'string',
},
},
type: 'string',
},
},
primaryPhoneCountryCode: {
@@ -93,6 +93,7 @@ export class ChartDataQueryService {
}: ExecuteGroupByQueryParams): Promise<GroupByRawResult[]> {
const gqlOperationFilter = convertChartFilterToGqlOperationFilter({
filter,
flatObjectMetadata,
flatFieldMetadataMaps,
userTimezone,
});
@@ -2,6 +2,7 @@ import {
type ChartFilter,
type CompositeFieldSubFieldName,
type FilterableAndTSVectorFieldType,
type PartialFieldMetadataItem,
type RecordFilterGroupLogicalOperator,
type ViewFilterOperand,
} from 'twenty-shared/types';
@@ -17,15 +18,18 @@ 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 => {
@@ -40,6 +44,34 @@ 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({
@@ -58,8 +90,6 @@ export const convertChartFilterToGqlOperationFilter = ({
subFieldName: (recordFilter.subFieldName ?? undefined) as
| CompositeFieldSubFieldName
| undefined,
relationTargetFieldMetadataId:
recordFilter.relationTargetFieldMetadataId ?? null,
};
},
);
@@ -74,11 +104,7 @@ export const convertChartFilterToGqlOperationFilter = ({
}));
return computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
fields,
recordFilters: convertedRecordFilters,
recordFilterGroups: convertedRecordFilterGroups,
filterValueDependencies: {
@@ -1,7 +1,12 @@
import { Injectable } from '@nestjs/common';
import {
type FieldMetadataComplexOption,
type FieldMetadataDefaultOption,
} from 'twenty-shared/types';
import {
computeRecordGqlOperationFilter,
isDefined,
isRecordFilterValueValid,
resolveInput,
} from 'twenty-shared/utils';
@@ -58,12 +63,38 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
const executionContext =
await this.workflowExecutionContextService.getExecutionContext(runInfo);
const { flatFieldMetadataMaps } =
const { flatObjectMetadata, 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)) {
@@ -79,11 +110,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
workflowActionInput.filter?.recordFilters &&
workflowActionInput.filter?.recordFilterGroups
? computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFieldMetadataMaps,
}),
fields,
recordFilters: workflowActionInput.filter.recordFilters,
recordFilterGroups: workflowActionInput.filter.recordFilterGroups,
filterValueDependencies: {
@@ -1,230 +0,0 @@
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);
});
});
@@ -1,348 +0,0 @@
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,7 +8,6 @@ export type ChartRecordFilter = {
type?: string;
recordFilterGroupId?: string | null;
subFieldName?: string | null;
relationTargetFieldMetadataId?: SerializedRelation | null;
};
export type ChartRecordFilterGroup = {
@@ -27,8 +27,7 @@ describe('computeRecordGqlOperationFilter', () => {
];
const filter = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) =>
id === companyIdField.id ? companyIdField : undefined,
fields: [companyIdField],
recordFilters,
recordFilterGroups: [],
filterValueDependencies: {
@@ -21,14 +21,11 @@ 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: [],
findFieldMetadataItemById,
fields,
recordFilterGroups: [],
currentRecordFilterGroupId: 'nonexistent',
});
@@ -48,7 +45,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
recordFilterGroupId: 'group1',
},
],
findFieldMetadataItemById,
fields,
recordFilterGroups: [
{
id: 'group1',
@@ -73,7 +70,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
recordFilterGroupId: 'group1',
},
],
findFieldMetadataItemById,
fields,
recordFilterGroups: [
{
id: 'group1',
@@ -98,7 +95,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
recordFilterGroupId: 'subgroup1',
},
],
findFieldMetadataItemById,
fields,
recordFilterGroups: [
{
id: 'group1',
@@ -131,9 +131,6 @@ 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,
@@ -159,7 +156,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'x',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeUndefined();
@@ -169,7 +166,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-text', RecordFilterOperand.CONTAINS, ''),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeUndefined();
@@ -184,7 +181,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'test',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ name: { ilike: '%test%' } });
@@ -198,7 +195,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.DOES_NOT_CONTAIN,
'test',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ not: { name: { ilike: '%test%' } } });
@@ -210,7 +207,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-number', RecordFilterOperand.IS, '42'),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ amount: { eq: 42 } });
@@ -220,7 +217,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-number', RecordFilterOperand.IS_NOT, '42'),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ not: { amount: { eq: 42 } } });
@@ -234,7 +231,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
'10',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ amount: { gte: 10 } });
@@ -248,7 +245,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.LESS_THAN_OR_EQUAL,
'100',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ amount: { lte: 100 } });
@@ -264,7 +261,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_AFTER,
'2024-03-15',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ createdAt: { gte: '2024-03-15' } });
@@ -278,7 +275,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_BEFORE,
'2024-03-15',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ createdAt: { lt: '2024-03-15' } });
@@ -292,7 +289,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS,
'2024-03-15',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ createdAt: { eq: '2024-03-15' } });
@@ -302,7 +299,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_IN_PAST, ''),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('createdAt.lt');
@@ -316,7 +313,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_IN_FUTURE,
'',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('createdAt.gte');
@@ -326,7 +323,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_TODAY, ''),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('createdAt.eq');
@@ -340,7 +337,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_RELATIVE,
'PAST_7_DAY',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
@@ -356,7 +353,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_AFTER,
'2024-03-15T10:00:00Z',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.gte');
@@ -370,7 +367,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_BEFORE,
'2024-03-15T10:00:00Z',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.lt');
@@ -384,7 +381,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS,
'2024-03-15T10:00:00Z',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
@@ -398,7 +395,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_IN_PAST,
'',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.lt');
@@ -412,7 +409,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_IN_FUTURE,
'',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.gt');
@@ -426,7 +423,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_TODAY,
'',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
@@ -440,7 +437,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_RELATIVE,
`PAST_7_DAY;;UTC;;`,
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
@@ -452,7 +449,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-rating', RecordFilterOperand.IS, '3'),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('rating.eq');
@@ -466,7 +463,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
'3',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('rating.in');
@@ -480,7 +477,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.LESS_THAN_OR_EQUAL,
'3',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('rating.in');
@@ -492,7 +489,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'true'),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ isActive: { eq: true } });
@@ -502,7 +499,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'false'),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ isActive: { eq: false } });
@@ -518,7 +515,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS,
'["ACTIVE","PENDING"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('status.in');
@@ -532,7 +529,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_NOT,
'["ACTIVE"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('not');
@@ -548,7 +545,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'["TAG1","TAG2"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('tags.containsAny');
@@ -562,7 +559,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.DOES_NOT_CONTAIN,
'["TAG1"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
@@ -578,7 +575,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('companyId.in');
@@ -592,7 +589,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS_NOT,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
@@ -608,7 +605,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'test',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ metadata: { like: '%test%' } });
@@ -622,7 +619,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.DOES_NOT_CONTAIN,
'test',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ not: { metadata: { like: '%test%' } } });
@@ -638,7 +635,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'doc',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ attachments: { like: '%doc%' } });
@@ -654,7 +651,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.VECTOR_SEARCH,
'hello world',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ search: { search: 'hello world' } });
@@ -672,7 +669,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
'CURRENCY',
'amountMicros',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('revenue');
@@ -688,7 +685,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'John',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
@@ -704,7 +701,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'Paris',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
@@ -720,7 +717,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'api',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({
@@ -747,7 +744,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'xyz123',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({
@@ -772,7 +769,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.DOES_NOT_CONTAIN,
'api',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({
@@ -803,7 +800,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.DOES_NOT_CONTAIN,
'xyz123',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({
@@ -834,7 +831,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
'PHONES',
'primaryPhoneNumber',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
@@ -852,7 +849,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
'EMAILS',
'primaryEmail',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
@@ -870,7 +867,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
'LINKS',
'primaryLinkUrl',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
@@ -886,7 +883,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.CONTAINS,
'["item1"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
@@ -900,7 +897,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.DOES_NOT_CONTAIN,
'["item1"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('not');
@@ -916,7 +913,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('recordId.in');
@@ -934,13 +931,13 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
relationTargetFieldMetadataId: 'f-text',
} as RecordFilter,
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ company: { name: { ilike: '%Acme%' } } });
});
// If the target field is no longer resolvable (e.g. it was
// If the target field is no longer in fieldMetadataItems (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.
@@ -951,7 +948,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
relationTargetFieldMetadataId: 'nonexistent-target',
} as RecordFilter,
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toBeUndefined();
@@ -966,7 +963,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
...makeFilter('f-relation', RecordFilterOperand.IS_EMPTY, ''),
relationTargetFieldMetadataId: 'f-text',
} as RecordFilter,
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
// Without traversal, the RELATION case would have produced a filter
@@ -987,7 +984,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
...makeFilter('f-relation', RecordFilterOperand.IS, '["ACTIVE"]'),
relationTargetFieldMetadataId: 'f-select',
} as RecordFilter,
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toEqual({ company: { status: { in: ['ACTIVE'] } } });
@@ -1004,7 +1001,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
RecordFilterOperand.IS,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
findFieldMetadataItemById,
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('companyId.in');
@@ -1,4 +1,5 @@
import {
type PartialFieldMetadataItem,
type RecordFilterValueDependencies,
type RecordGqlOperationFilter,
} from '@/types';
@@ -7,20 +8,17 @@ import {
type RecordFilter,
type RecordFilterGroup,
} from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter';
import {
type FindFieldMetadataItemById,
turnRecordFilterIntoRecordGqlOperationFilter,
} from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
import { isDefined } from '@/utils/validation/isDefined';
export const computeRecordGqlOperationFilter = ({
findFieldMetadataItemById,
fields,
recordFilters,
recordFilterGroups,
filterValueDependencies,
}: {
recordFilters: Omit<RecordFilter, 'id'>[];
findFieldMetadataItemById: FindFieldMetadataItemById;
fields: PartialFieldMetadataItem[];
recordFilterGroups: RecordFilterGroup[];
filterValueDependencies: RecordFilterValueDependencies;
}): RecordGqlOperationFilter => {
@@ -30,7 +28,7 @@ export const computeRecordGqlOperationFilter = ({
.map((regularFilter) => {
return turnRecordFilterIntoRecordGqlOperationFilter({
recordFilter: regularFilter,
findFieldMetadataItemById,
fieldMetadataItems: fields,
filterValueDependencies,
});
})
@@ -44,7 +42,7 @@ export const computeRecordGqlOperationFilter = ({
turnRecordFilterGroupsIntoGqlOperationFilter({
filterValueDependencies,
filters: recordFilters,
findFieldMetadataItemById,
fields,
recordFilterGroups,
currentRecordFilterGroupId: outermostFilterGroupId,
});
@@ -221,13 +221,11 @@ export const turnAnyFieldFilterIntoRecordGqlFilter = ({
}
}
const fieldById = new Map(fields.map((field) => [field.id, field]));
const baseRecordGqlOperationFilters = anyFieldRecordFilters
.map((recordFilter) =>
turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies: {},
findFieldMetadataItemById: (id) => fieldById.get(id),
fieldMetadataItems: fields,
recordFilter,
}),
)
@@ -1,6 +1,7 @@
import {
type CompositeFieldSubFieldName,
type FilterableAndTSVectorFieldType,
type PartialFieldMetadataItem,
RecordFilterGroupLogicalOperator,
type RecordFilterValueDependencies,
type RecordGqlOperationFilter,
@@ -8,10 +9,7 @@ import {
} from '@/types';
import { isDefined } from '@/utils';
import {
type FindFieldMetadataItemById,
turnRecordFilterIntoRecordGqlOperationFilter,
} from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
export type RecordFilter = {
id: string;
@@ -33,13 +31,13 @@ export type RecordFilterGroup = {
export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
filterValueDependencies,
filters,
findFieldMetadataItemById,
fields,
recordFilterGroups,
currentRecordFilterGroupId,
}: {
filterValueDependencies: RecordFilterValueDependencies;
filters: Omit<RecordFilter, 'id'>[];
findFieldMetadataItemById: FindFieldMetadataItemById;
fields: PartialFieldMetadataItem[];
recordFilterGroups: RecordFilterGroup[];
currentRecordFilterGroupId?: string;
}): RecordGqlOperationFilter | undefined => {
@@ -60,7 +58,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: recordFilter,
findFieldMetadataItemById,
fieldMetadataItems: fields,
}),
)
.filter(isDefined);
@@ -75,7 +73,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
turnRecordFilterGroupsIntoGqlOperationFilter({
filterValueDependencies,
filters,
findFieldMetadataItemById,
fields,
recordFilterGroups,
currentRecordFilterGroupId: subRecordFilterGroup.id,
}),
@@ -62,23 +62,21 @@ type FieldShared = {
label: string;
};
export type FindFieldMetadataItemById = (id: string) => FieldShared | undefined;
type TurnRecordFilterIntoRecordGqlOperationFilterParams = {
filterValueDependencies: RecordFilterValueDependencies;
recordFilter: Omit<RecordFilter, 'id'>;
findFieldMetadataItemById: FindFieldMetadataItemById;
fieldMetadataItems: FieldShared[];
};
export const turnRecordFilterIntoRecordGqlOperationFilter = ({
recordFilter,
findFieldMetadataItemById,
fieldMetadataItems,
filterValueDependencies,
}: TurnRecordFilterIntoRecordGqlOperationFilterParams):
| RecordGqlOperationFilter
| undefined => {
const sourceFieldMetadataItem = findFieldMetadataItemById(
recordFilter.fieldMetadataId,
const sourceFieldMetadataItem = fieldMetadataItems.find(
(field) => field.id === recordFilter.fieldMetadataId,
);
if (!isDefined(sourceFieldMetadataItem)) {
@@ -93,8 +91,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
sourceFieldMetadataItem.type === FieldMetadataType.RELATION &&
isDefined(recordFilter.relationTargetFieldMetadataId)
) {
const targetFieldMetadataItem = findFieldMetadataItemById(
recordFilter.relationTargetFieldMetadataId,
const targetFieldMetadataItem = fieldMetadataItems.find(
(field) => field.id === recordFilter.relationTargetFieldMetadataId,
);
if (!isDefined(targetFieldMetadataItem)) {
@@ -104,7 +104,6 @@ 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';