[GQL_VIEW_FILTER_API_BREAKING_CHANGE][WHEN_RELEASED_REQUIRES_CACHE_FLUSH] ViewFilter migration to workspace migration v2 (#15010)

# Introduction
Migrating `viewFilter` to v2 in order to migrate later the field update
side effect on view to v2 too

## What's done
- Created flat-view-filter
- flat view filter runner
- flat view filter builder
- create view filter service v2 and input transpilers
- refactor the existing view filter resolver to fix standard (
BREAKING_CHANGE on graphql api update especially ) REST stays the same
- refactored the front to consume the mutations autogenerated

## New generic tools
### Compare two flat entity
Introducing a new util to compare two flat entity, it's strictly typed
and will be added to the generic builder in a following PR
This will ease flat entity addition as won't required to create a
specific abstraction for comparison
Generic builder will expect specific constant: properties to compare and
properties to stringify

### Transform flat entity for comparison
Forked and refactor the initial existing method for flat entity business
scope and type safety

## Coverage
Migrated existing integration tests to fit new contract API
This PR does not add strong coverage on validation exceptions
Deadlines are too short

close https://github.com/twentyhq/core-team-issues/issues/1666
This commit is contained in:
Paul Rastoin
2025-10-10 15:02:14 +02:00
committed by GitHub
parent 37473175a0
commit 651ab184a7
97 changed files with 3012 additions and 620 deletions
@@ -10,4 +10,5 @@ export const ALL_FLAT_ENTITY_MAPS_PROPERTIES = [
'flatCronTriggerMaps',
'flatRouteTriggerMaps',
'flatFieldMetadataMaps',
'flatViewFilterMaps',
] as const satisfies (keyof AllFlatEntityMaps)[];
@@ -11,4 +11,5 @@ export const EMPTY_ALL_FLAT_ENTITY_MAPS = {
flatCronTriggerMaps: EMPTY_FLAT_ENTITY_MAPS,
flatDatabaseEventTriggerMaps: EMPTY_FLAT_ENTITY_MAPS,
flatRouteTriggerMaps: EMPTY_FLAT_ENTITY_MAPS,
flatViewFilterMaps: EMPTY_FLAT_ENTITY_MAPS,
} as const satisfies AllFlatEntityMaps;
@@ -4,6 +4,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
@@ -18,4 +19,5 @@ export type AllFlatEntitiesByMetadataEngineName = {
cronTrigger: FlatCronTrigger;
databaseEventTrigger: FlatDatabaseEventTrigger;
routeTrigger: FlatRouteTrigger;
viewFilter: FlatViewFilter;
};
@@ -0,0 +1,77 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { parseJson } from 'twenty-shared/utils';
import { type AllFlatEntities } from 'src/engine/core-modules/common/types/all-flat-entities.type';
import { transformFlatEntityForComparison } from 'src/engine/core-modules/common/utils/transform-flat-entity-for-comparison.util';
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
export const compareTwoFlatEntity = <
TFlatEntity extends AllFlatEntities,
PToCompare extends keyof TFlatEntity,
PJsonB extends keyof TFlatEntity,
>({
fromFlatEntity,
toFlatEntity,
propertiesToCompare,
propertiesToStringify,
}: {
propertiesToCompare: PToCompare[];
propertiesToStringify: PJsonB[];
} & FromTo<TFlatEntity, 'flatEntity'>) => {
const [transformedFromFlatEntity, transformedToFlatEntity] = [
fromFlatEntity,
toFlatEntity,
].map((flatEntity) =>
transformFlatEntityForComparison({
flatEntity,
options: {
propertiesToCompare,
propertiesToStringify,
},
}),
);
const flatEntityDifferences = diff(
transformedFromFlatEntity,
transformedToFlatEntity,
);
return flatEntityDifferences.flatMap<
Array<
{
[P in PToCompare]: PropertyUpdate<TFlatEntity, P>;
}[PToCompare]
>[number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as PToCompare;
const isJsonb = propertiesToStringify.includes(
property as unknown as PJsonB,
);
if (isJsonb) {
return {
from: parseJson(oldValue),
to: parseJson(value),
property,
};
}
return {
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
});
};
@@ -0,0 +1,40 @@
import { type AllFlatEntities } from 'src/engine/core-modules/common/types/all-flat-entities.type';
import { orderObjectProperties } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/order-object-properties.util';
export function transformFlatEntityForComparison<
TFlatEntity extends AllFlatEntities,
PToCompare extends keyof TFlatEntity,
PJsonB extends keyof TFlatEntity,
>({
flatEntity,
options: { propertiesToCompare, propertiesToStringify },
}: {
flatEntity: TFlatEntity;
options: {
propertiesToCompare: PToCompare[];
propertiesToStringify: PJsonB[];
};
}) {
return propertiesToCompare.reduce(
(flatEntityAccumulator, propertyToCompare) => {
const currentValue = flatEntity[propertyToCompare];
if (
propertiesToStringify.includes(propertyToCompare as unknown as PJsonB)
) {
const orderedValue = orderObjectProperties(currentValue);
return {
...flatEntityAccumulator,
[propertyToCompare]: JSON.stringify(orderedValue),
};
}
return {
...flatEntityAccumulator,
[propertyToCompare]: currentValue,
};
},
{} as Pick<TFlatEntity, PToCompare>,
);
}
@@ -24,6 +24,7 @@ export const throwOnFieldInputTranspilationsError: ThrowOnFieldInputTranspilatio
throw new WorkspaceMigrationBuilderExceptionV2(
{
report: {
viewFilter: [],
fieldMetadata: [],
index: [],
view: [],
@@ -1,11 +1,8 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { compareTwoFlatEntity } from 'src/engine/core-modules/common/utils/compare-two-flat-entity.util';
import { FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-properties-to-compare.constant';
import { type FlatViewFieldPropertiesToCompare } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-properties-to-compare.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type GetWorkspaceMigrationUpdateViewFieldActionArgs = FromTo<
FlatViewField,
@@ -16,44 +13,10 @@ export const compareTwoFlatViewField = ({
fromFlatViewField,
toFlatViewField,
}: GetWorkspaceMigrationUpdateViewFieldActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE.includes(
property as FlatViewFieldPropertiesToCompare,
),
return compareTwoFlatEntity({
fromFlatEntity: fromFlatViewField,
toFlatEntity: toFlatViewField,
propertiesToStringify: [],
};
const fromCompare = transformMetadataForComparison(
fromFlatViewField,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
toFlatViewField,
transformMetadataForComparisonParameters,
);
const flatViewFieldDifferences = diff(fromCompare, toCompare);
return flatViewFieldDifferences.flatMap<
UpdateViewFieldAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatViewFieldPropertiesToCompare;
return {
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
propertiesToCompare: FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE,
});
};
@@ -0,0 +1,10 @@
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
export const FLAT_VIEW_FILTER_EDITABLE_PROPERTIES = [
'fieldMetadataId',
'operand',
'value',
'viewFilterGroupId',
'positionInViewFilterGroup',
'subFieldName',
] as const satisfies (keyof FlatViewFilter)[];
@@ -0,0 +1,5 @@
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
export const FLAT_VIEW_FILTER_JSONB_PROPERTIES = [
'value',
] as const satisfies (keyof FlatViewFilter)[];
@@ -0,0 +1,8 @@
import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
export const FLAT_VIEW_FILTER_PROPERTIES_TO_COMPARE = [
'viewId',
'deletedAt',
...FLAT_VIEW_FILTER_EDITABLE_PROPERTIES,
] as const satisfies (keyof FlatViewFilter)[];
@@ -0,0 +1,8 @@
import { type ViewFilterEntityRelationProperties } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
export const VIEW_FILTER_ENTITY_RELATION_PROPERTIES = [
'fieldMetadata',
'view',
'viewFilterGroup',
'workspace',
] as const satisfies ViewFilterEntityRelationProperties[];
@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type';
import { fromViewFilterEntityToFlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
@WorkspaceFlatMapCache('flatViewFilterMaps')
export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceFlatMapCacheService<FlatViewFilterMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewFilterEntity)
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewFilterMaps> {
const viewFilters = await this.viewFilterRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
return viewFilters.reduce((flatViewFilterMaps, viewFilterEntity) => {
const flatViewFilter =
fromViewFilterEntityToFlatViewFilter(viewFilterEntity);
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFilter,
flatEntityMaps: flatViewFilterMaps,
});
}, EMPTY_FLAT_ENTITY_MAPS);
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
export type FlatViewFilterMaps = FlatEntityMaps<FlatViewFilter>;
@@ -0,0 +1,4 @@
import { type FLAT_VIEW_FILTER_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-properties-to-compare.constant';
export type FlatViewFilterPropertiesToCompare =
(typeof FLAT_VIEW_FILTER_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,19 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type ViewFilterEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ViewFilterEntity,
FieldMetadataEntity | ViewEntity | ViewFilterGroupEntity | Workspace
>;
export type FlatViewFilter = Omit<
ViewFilterEntity,
ViewFilterEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,23 @@
import { type FromTo } from 'twenty-shared/types';
import { compareTwoFlatEntity } from 'src/engine/core-modules/common/utils/compare-two-flat-entity.util';
import { FLAT_VIEW_FILTER_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-jsonb-properties.constant';
import { FLAT_VIEW_FILTER_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-properties-to-compare.constant';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
type GetWorkspaceMigrationUpdateViewFilterActionArgs = FromTo<
FlatViewFilter,
'FlatViewFilter'
>;
export const compareTwoFlatViewFilter = ({
fromFlatViewFilter,
toFlatViewFilter,
}: GetWorkspaceMigrationUpdateViewFilterActionArgs) => {
return compareTwoFlatEntity({
fromFlatEntity: fromFlatViewFilter,
toFlatEntity: toFlatViewFilter,
propertiesToStringify: FLAT_VIEW_FILTER_JSONB_PROPERTIES,
propertiesToCompare: FLAT_VIEW_FILTER_PROPERTIES_TO_COMPARE,
});
};
@@ -0,0 +1,47 @@
import { ViewFilterOperand } from 'twenty-shared/types';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type CreateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/create-view-filter.input';
export const fromCreateViewFilterInputToFlatViewFilterToCreate = ({
createViewFilterInput: rawCreateViewFilterInput,
workspaceId,
}: {
createViewFilterInput: CreateViewFilterInput;
workspaceId: string;
}): FlatViewFilter => {
const { fieldMetadataId, viewId, value, ...createViewFilterInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateViewFilterInput,
[
'fieldMetadataId',
'id',
'viewId',
'viewFilterGroupId',
'operand',
'subFieldName',
],
);
const createdAt = new Date();
const viewFilterId = createViewFilterInput.id ?? v4();
return {
id: viewFilterId,
fieldMetadataId,
viewId,
workspaceId,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: viewFilterId,
operand: createViewFilterInput.operand ?? ViewFilterOperand.CONTAINS,
value: value,
viewFilterGroupId: createViewFilterInput.viewFilterGroupId ?? null,
positionInViewFilterGroup:
createViewFilterInput.positionInViewFilterGroup ?? null,
subFieldName: createViewFilterInput.subFieldName ?? null,
};
};
@@ -0,0 +1,40 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type DeleteViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/delete-view-filter.input';
import {
ViewFilterException,
ViewFilterExceptionCode,
} from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
export const fromDeleteViewFilterInputToFlatViewFilterOrThrow = ({
deleteViewFilterInput: rawDeleteViewFilterInput,
flatViewFilterMaps,
}: {
deleteViewFilterInput: DeleteViewFilterInput;
flatViewFilterMaps: FlatViewFilterMaps;
}): FlatViewFilter => {
const { id: viewFilterId } = extractAndSanitizeObjectStringFields(
rawDeleteViewFilterInput,
['id'],
);
const existingFlatViewFilterToDelete = flatViewFilterMaps.byId[viewFilterId];
if (!isDefined(existingFlatViewFilterToDelete)) {
throw new ViewFilterException(
t`View filter to delete not found`,
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
);
}
return {
...existingFlatViewFilterToDelete,
deletedAt: new Date(),
};
};
@@ -0,0 +1,37 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type DestroyViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/destroy-view-filter.input';
import {
ViewFilterException,
ViewFilterExceptionCode,
} from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
export const fromDestroyViewFilterInputToFlatViewFilterOrThrow = ({
destroyViewFilterInput,
flatViewFilterMaps,
}: {
destroyViewFilterInput: DestroyViewFilterInput;
flatViewFilterMaps: FlatViewFilterMaps;
}): FlatViewFilter => {
const { id: viewFilterId } = extractAndSanitizeObjectStringFields(
destroyViewFilterInput,
['id'],
);
const existingFlatViewFilterToDestroy = flatViewFilterMaps.byId[viewFilterId];
if (!isDefined(existingFlatViewFilterToDestroy)) {
throw new ViewFilterException(
t`View filter to destroy not found`,
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
);
}
return existingFlatViewFilterToDestroy;
};
@@ -0,0 +1,51 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant';
import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type UpdateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/update-view-filter.input';
import {
ViewFilterException,
ViewFilterExceptionCode,
} from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateViewFilterInputToFlatViewFilterToUpdateOrThrow = ({
updateViewFilterInput: rawUpdateViewFilterInput,
flatViewFilterMaps,
}: {
updateViewFilterInput: UpdateViewFilterInput;
flatViewFilterMaps: FlatViewFilterMaps;
}): FlatViewFilter => {
const { id: viewFilterToUpdateId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateViewFilterInput,
['id'],
);
const existingFlatViewFilterToUpdate =
flatViewFilterMaps.byId[viewFilterToUpdateId];
if (!isDefined(existingFlatViewFilterToUpdate)) {
throw new ViewFilterException(
t`View filter to update not found`,
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
);
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdateViewFilterInput.update,
FLAT_VIEW_FILTER_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatViewFilterToUpdate,
properties: FLAT_VIEW_FILTER_EDITABLE_PROPERTIES,
update: updatedEditableFieldProperties,
});
};
@@ -0,0 +1,21 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { VIEW_FILTER_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/view-filter-entity-relation-properties.constant';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
export const fromViewFilterEntityToFlatViewFilter = (
viewFilterEntity: ViewFilterEntity,
): FlatViewFilter => {
const viewFilterEntityWithoutRelations = removePropertiesFromRecord(
viewFilterEntity,
VIEW_FILTER_ENTITY_RELATION_PROPERTIES,
);
return {
...viewFilterEntityWithoutRelations,
universalIdentifier:
viewFilterEntityWithoutRelations.universalIdentifier ??
viewFilterEntityWithoutRelations.id,
};
};
@@ -39,8 +39,11 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceFlatMapCacheServi
viewFields: {
id: true,
},
viewFilters: {
id: true,
},
},
relations: ['viewFields'],
relations: ['viewFields', 'viewFilters'],
withDeleted: true,
});
@@ -23,4 +23,5 @@ export type ViewEntityRelationProperties =
export type FlatView = Omit<ViewEntity, ViewEntityRelationProperties> & {
universalIdentifier: string;
viewFieldIds: string[];
viewFilterIds: string[];
};
@@ -44,5 +44,6 @@ export const fromCreateViewInputToFlatViewToCreate = ({
type: createViewInput.type ?? ViewType.TABLE,
universalIdentifier: v4(),
viewFieldIds: [],
viewFilterIds: [],
};
};
@@ -1,38 +0,0 @@
import { v4 } from 'uuid';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
export const fromPartialFlatViewToFlatViewWithDefault = (
partialFlatView: Partial<FlatView>,
): FlatView => {
const createdAt = new Date();
const viewId = partialFlatView.id ?? v4();
return {
...partialFlatView,
id: viewId,
name: partialFlatView.name ?? '',
objectMetadataId: partialFlatView.objectMetadataId ?? '',
type: partialFlatView.type ?? ViewType.TABLE,
key: partialFlatView.key ?? null,
icon: partialFlatView.icon ?? '',
position: partialFlatView.position ?? 0,
isCompact: partialFlatView.isCompact ?? false,
isCustom: partialFlatView.isCustom ?? false,
openRecordIn: partialFlatView.openRecordIn ?? ViewOpenRecordIn.SIDE_PANEL,
kanbanAggregateOperation: partialFlatView.kanbanAggregateOperation ?? null,
kanbanAggregateOperationFieldMetadataId:
partialFlatView.kanbanAggregateOperationFieldMetadataId ?? null,
workspaceId: partialFlatView.workspaceId ?? '',
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
anyFieldFilterValue: partialFlatView.anyFieldFilterValue ?? null,
universalIdentifier: partialFlatView.universalIdentifier ?? viewId,
viewFieldIds: partialFlatView.viewFieldIds ?? [],
calendarLayout: partialFlatView.calendarLayout ?? null,
calendarFieldMetadataId: partialFlatView.calendarFieldMetadataId ?? null,
};
};
@@ -15,6 +15,7 @@ export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => {
universalIdentifier:
viewEntityWithoutRelations.universalIdentifier ??
viewEntityWithoutRelations.id,
viewFieldIds: viewEntity.viewFields.map((viewField) => viewField.id) ?? [],
viewFieldIds: viewEntity.viewFields.map((viewField) => viewField.id),
viewFilterIds: viewEntity.viewFilters.map((viewFilter) => viewFilter.id),
};
};
@@ -89,10 +89,15 @@ export class ViewFilterController {
@Body() input: UpdateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO> {
const updatedViewFilter = await this.viewFilterService.update(
const updateInput: UpdateViewFilterInput = {
id,
update: input.update ?? input,
};
const updatedViewFilter = await this.viewFilterService.update(
updateInput.id,
workspace.id,
input,
updateInput.update,
);
return updatedViewFilter;
@@ -2,33 +2,54 @@ import { Field, InputType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { ViewFilterOperand } from 'twenty-shared/types';
import {
IsDefined,
IsEnum,
IsNumber,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type';
@InputType()
export class CreateViewFilterInput {
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsUUID()
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@IsOptional()
@IsEnum(ViewFilterOperand)
@Field({ nullable: true, defaultValue: ViewFilterOperand.CONTAINS })
operand?: ViewFilterOperand;
@IsDefined()
@Field(() => GraphQLJSON, { nullable: false })
value: ViewFilterValue;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
viewFilterGroupId?: string;
@IsOptional()
@IsNumber()
@Field({ nullable: true })
positionInViewFilterGroup?: number;
@IsOptional()
@IsString()
@Field({ nullable: true })
subFieldName?: string;
@IsUUID()
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
}
@@ -0,0 +1,15 @@
import { InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class DeleteViewFilterInput {
@IDField(() => UUIDScalarType, {
description: 'The id of the view filter to delete.',
})
@IsUUID()
id: string;
}
@@ -0,0 +1,15 @@
import { InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class DestroyViewFilterInput {
@IDField(() => UUIDScalarType, {
description: 'The id of the view filter to destroy.',
})
@IsUUID()
id: string;
}
@@ -1,6 +1,66 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { Field, InputType } from '@nestjs/graphql';
import { CreateViewFilterInput } from './create-view-filter.input';
import {
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { ViewFilterOperand } from 'twenty-shared/types';
import { Type } from 'class-transformer';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type';
@InputType()
export class UpdateViewFilterInput extends PartialType(CreateViewFilterInput) {}
class UpdateViewFilterInputUpdates {
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
fieldMetadataId?: string;
@IsOptional()
@IsEnum(ViewFilterOperand)
@Field({ nullable: true })
operand?: ViewFilterOperand;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
value?: ViewFilterValue;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
viewFilterGroupId?: string;
@IsOptional()
@IsNumber()
@Field({ nullable: true })
positionInViewFilterGroup?: number;
@IsOptional()
@IsString()
@Field({ nullable: true })
subFieldName?: string;
}
@InputType()
export class UpdateViewFilterInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the view filter to update',
})
id: string;
@Type(() => UpdateViewFilterInputUpdates)
@ValidateNested()
@Field(() => UpdateViewFilterInputUpdates, {
description: 'The view filter to update',
})
update: UpdateViewFilterInputUpdates;
}
@@ -26,7 +26,10 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
where: '"deletedAt" IS NULL',
})
@Index('IDX_VIEW_FILTER_FIELD_METADATA_ID', ['fieldMetadataId'])
export class ViewFilterEntity extends SyncableEntity {
export class ViewFilterEntity
extends SyncableEntity
implements Required<ViewFilterEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -51,13 +54,13 @@ export class ViewFilterEntity extends SyncableEntity {
value: ViewFilterValue;
@Column({ nullable: true, type: 'uuid' })
viewFilterGroupId?: string | null;
viewFilterGroupId: string | null;
@Column({ nullable: true, type: 'double precision' })
positionInViewFilterGroup: number | null;
@Column({ nullable: true, type: 'text', default: null })
subFieldName?: string | null;
subFieldName: string | null;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@@ -72,7 +75,7 @@ export class ViewFilterEntity extends SyncableEntity {
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
deletedAt: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
@@ -1,14 +1,17 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/create-view-filter.input';
import { DeleteViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/delete-view-filter.input';
import { DestroyViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/destroy-view-filter.input';
import { UpdateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/update-view-filter.input';
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
import { ViewFilterV2Service } from 'src/engine/metadata-modules/view-filter/services/view-filter-v2.service';
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
@@ -16,7 +19,11 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFilterResolver {
constructor(private readonly viewFilterService: ViewFilterService) {}
constructor(
private readonly viewFilterService: ViewFilterService,
private readonly featureFlagService: FeatureFlagService,
private readonly viewFilterV2Service: ViewFilterV2Service,
) {}
@Query(() => [ViewFilterDTO])
async getCoreViewFilters(
@@ -41,47 +48,104 @@ export class ViewFilterResolver {
@Mutation(() => ViewFilterDTO)
async createCoreViewFilter(
@Args('input') input: CreateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
@Args('input') createViewFilterInput: CreateViewFilterInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFilterDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFilterV2Service.createOne({
createViewFilterInput,
workspaceId,
});
}
return this.viewFilterService.create({
...input,
workspaceId: workspace.id,
...createViewFilterInput,
workspaceId,
});
}
@Mutation(() => ViewFilterDTO)
async updateCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
@Args('input') updateViewFilterInput: UpdateViewFilterInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFilterDTO> {
return this.viewFilterService.update(id, workspace.id, input);
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFilterV2Service.updateOne({
updateViewFilterInput,
workspaceId,
});
}
return this.viewFilterService.update(
updateViewFilterInput.id,
workspaceId,
updateViewFilterInput.update,
);
}
@Mutation(() => Boolean)
@Mutation(() => ViewFilterDTO)
async deleteCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
@Args('input') deleteViewFilterInput: DeleteViewFilterInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFilterDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFilterV2Service.deleteOne({
deleteViewFilterInput,
workspaceId,
});
}
const deletedViewFilter = await this.viewFilterService.delete(
id,
workspace.id,
deleteViewFilterInput.id,
workspaceId,
);
return isDefined(deletedViewFilter);
return deletedViewFilter;
}
@Mutation(() => Boolean)
@Mutation(() => ViewFilterDTO)
async destroyCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedViewFilter = await this.viewFilterService.destroy(
id,
workspace.id,
@Args('input') destroyViewFilterInput: DestroyViewFilterInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFilterDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
const destroyedViewFilter = await this.viewFilterV2Service.destroyOne({
destroyViewFilterInput,
workspaceId,
});
return destroyedViewFilter;
}
const destroyedViewFilter = await this.viewFilterService.destroy(
destroyViewFilterInput.id,
workspaceId,
);
return isDefined(deletedViewFilter);
return destroyedViewFilter;
}
}
@@ -333,7 +333,7 @@ describe('ViewFilterService', () => {
expect(viewFilterService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFilterRepository.delete).toHaveBeenCalledWith(id);
expect(result).toEqual(true);
expect(result).toEqual(mockViewFilter);
});
});
});
@@ -0,0 +1,326 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { fromCreateViewFilterInputToFlatViewFilterToCreate } from 'src/engine/metadata-modules/flat-view-filter/utils/from-create-view-filter-input-to-flat-view-filter-to-create.util';
import { fromDeleteViewFilterInputToFlatViewFilterOrThrow } from 'src/engine/metadata-modules/flat-view-filter/utils/from-delete-view-filter-input-to-flat-view-filter-or-throw.util';
import { fromDestroyViewFilterInputToFlatViewFilterOrThrow } from 'src/engine/metadata-modules/flat-view-filter/utils/from-destroy-view-filter-input-to-flat-view-filter-or-throw.util';
import { fromUpdateViewFilterInputToFlatViewFilterToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view-filter/utils/from-update-view-filter-input-to-flat-view-filter-to-update-or-throw.util';
import { CreateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/create-view-filter.input';
import { DeleteViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/delete-view-filter.input';
import { DestroyViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/destroy-view-filter.input';
import { UpdateViewFilterInput } from 'src/engine/metadata-modules/view-filter/dtos/inputs/update-view-filter.input';
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ViewFilterV2Service {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async createOne({
createViewFilterInput,
workspaceId,
}: {
createViewFilterInput: CreateViewFilterInput;
workspaceId: string;
}): Promise<ViewFilterDTO> {
const {
flatViewFilterMaps: existingFlatViewFilterMaps,
flatViewMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFilterMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
'flatObjectMetadataMaps',
],
},
);
const flatViewFilterToCreate =
fromCreateViewFilterInputToFlatViewFilterToCreate({
createViewFilterInput,
workspaceId,
});
const toFlatViewFilterMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFilterToCreate,
flatEntityMaps: existingFlatViewFilterMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatViewFilterMaps: {
from: existingFlatViewFilterMaps,
to: toFlatViewFilterMaps,
},
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps,
flatViewMaps,
flatObjectMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating view filter',
);
}
const { flatViewFilterMaps: recomputedExistingFlatViewFilterMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFilterMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFilterToCreate.id,
flatEntityMaps: recomputedExistingFlatViewFilterMaps,
});
}
async updateOne({
updateViewFilterInput,
workspaceId,
}: {
workspaceId: string;
updateViewFilterInput: UpdateViewFilterInput;
}): Promise<ViewFilterDTO> {
const {
flatViewFilterMaps: existingFlatViewFilterMaps,
flatViewMaps,
flatFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFilterMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
],
},
);
const optimisticallyUpdatedFlatViewFilter =
fromUpdateViewFilterInputToFlatViewFilterToUpdateOrThrow({
flatViewFilterMaps: existingFlatViewFilterMaps,
updateViewFilterInput,
});
const toFlatViewFilterMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatViewFilter,
flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatViewFilterMaps: {
from: existingFlatViewFilterMaps,
to: toFlatViewFilterMaps,
},
},
dependencyAllFlatEntityMaps: {
flatViewMaps,
flatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating view filter',
);
}
const { flatViewFilterMaps: recomputedExistingFlatViewFilterMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFilterMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatViewFilter.id,
flatEntityMaps: recomputedExistingFlatViewFilterMaps,
});
}
async deleteOne({
deleteViewFilterInput,
workspaceId,
}: {
deleteViewFilterInput: DeleteViewFilterInput;
workspaceId: string;
}): Promise<ViewFilterDTO> {
const {
flatViewFilterMaps: existingFlatViewFilterMaps,
flatViewMaps,
flatFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFilterMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
],
},
);
const optimisticallyUpdatedFlatViewFilterWithDeletedAt =
fromDeleteViewFilterInputToFlatViewFilterOrThrow({
flatViewFilterMaps: existingFlatViewFilterMaps,
deleteViewFilterInput,
});
const toFlatViewFilterMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatViewFilterWithDeletedAt,
flatEntityMaps: existingFlatViewFilterMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatViewFilterMaps: {
from: existingFlatViewFilterMaps,
to: toFlatViewFilterMaps,
},
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps,
flatViewMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view filter',
);
}
const { flatViewFilterMaps: recomputedExistingFlatViewFilterMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFilterMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatViewFilterWithDeletedAt.id,
flatEntityMaps: recomputedExistingFlatViewFilterMaps,
});
}
async destroyOne({
destroyViewFilterInput,
workspaceId,
}: {
destroyViewFilterInput: DestroyViewFilterInput;
workspaceId: string;
}): Promise<ViewFilterDTO> {
const {
flatViewFilterMaps: existingFlatViewFilterMaps,
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFilterMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
],
},
);
const existingViewFilterToDelete =
fromDestroyViewFilterInputToFlatViewFilterOrThrow({
destroyViewFilterInput,
flatViewFilterMaps: existingFlatViewFilterMaps,
});
const fromFlatViewFilterMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [existingViewFilterToDelete.id],
flatEntityMaps: existingFlatViewFilterMaps,
});
const toFlatViewFilterMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: fromFlatViewFilterMaps,
entityToDeleteId: existingViewFilterToDelete.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatViewFilterMaps: {
from: fromFlatViewFilterMaps,
to: toFlatViewFilterMaps,
},
},
dependencyAllFlatEntityMaps: {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view filter',
);
}
return existingViewFilterToDelete;
}
}
@@ -168,7 +168,7 @@ export class ViewFilterService {
return viewFilter;
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
async destroy(id: string, workspaceId: string): Promise<ViewFilterEntity> {
const viewFilter = await this.findById(id, workspaceId);
if (!isDefined(viewFilter)) {
@@ -185,7 +185,7 @@ export class ViewFilterService {
await this.flushGraphQLCache(workspaceId);
return true;
return viewFilter;
}
private async flushGraphQLCache(workspaceId: string): Promise<void> {
@@ -1,19 +1,27 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ViewFilterController } from 'src/engine/metadata-modules/view-filter/controllers/view-filter.controller';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewFilterResolver } from 'src/engine/metadata-modules/view-filter/resolvers/view-filter.resolver';
import { ViewFilterV2Service } from 'src/engine/metadata-modules/view-filter/services/view-filter-v2.service';
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
TypeOrmModule.forFeature([ViewFilterEntity]),
TypeOrmModule.forFeature([ViewFilterEntity, ViewEntity]),
WorkspaceCacheStorageModule,
FeatureFlagModule,
WorkspaceMigrationV2Module,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
controllers: [ViewFilterController],
providers: [ViewFilterService, ViewFilterResolver],
exports: [ViewFilterService],
providers: [ViewFilterService, ViewFilterResolver, ViewFilterV2Service],
exports: [ViewFilterService, ViewFilterV2Service],
})
export class ViewFilterModule {}
@@ -7,10 +7,12 @@ import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-m
import { WorkspaceFlatIndexMapCacheService } from 'src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service';
import { WorkspaceFlatObjectMetadataMapCacheService } from 'src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service';
import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service';
import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service';
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
@@ -26,6 +28,7 @@ import { WorkspaceFlatMapCacheRegistryService } from 'src/engine/workspace-flat-
TypeOrmModule.forFeature([
ViewEntity,
ViewFieldEntity,
ViewFilterEntity,
IndexMetadataEntity,
FieldMetadataEntity,
ObjectMetadataEntity,
@@ -36,6 +39,7 @@ import { WorkspaceFlatMapCacheRegistryService } from 'src/engine/workspace-flat-
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
],
@@ -44,6 +48,7 @@ import { WorkspaceFlatMapCacheRegistryService } from 'src/engine/workspace-flat-
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
],
@@ -19,4 +19,5 @@ export const EMPTY_ORCHESTRATOR_ACTIONS_REPORT = {
databaseEventTrigger: emptyCreatedDeletedUpdated,
cronTrigger: emptyCreatedDeletedUpdated,
routeTrigger: emptyCreatedDeletedUpdated,
viewFilter: emptyCreatedDeletedUpdated,
} as const satisfies OrchestratorActionsReport;
@@ -10,6 +10,7 @@ export const fromWorkspaceMigrationBuilderExceptionToValidationResponseError = (
): ValidationErrorResponse => {
const emptyResponseError: ValidationErrorResponse = {
summary: {
invalidViewFilter: 0,
invalidObjectMetadata: 0,
invalidView: 0,
invalidViewField: 0,
@@ -31,6 +32,7 @@ export const fromWorkspaceMigrationBuilderExceptionToValidationResponseError = (
databaseEventTrigger: [],
cronTrigger: [],
routeTrigger: [],
viewFilter: [],
},
};
@@ -20,6 +20,7 @@ import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/work
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
import { WorkspaceMigrationV2ViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service';
import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
@Injectable()
export class WorkspaceMigrationBuildOrchestratorService {
@@ -28,6 +29,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationV2IndexActionsBuilderService: WorkspaceMigrationV2IndexActionsBuilderService,
private readonly workspaceMigrationV2ViewActionsBuilderService: WorkspaceMigrationV2ViewActionsBuilderService,
private readonly workspaceMigrationV2ViewFieldActionsBuilderService: WorkspaceMigrationV2ViewFieldActionsBuilderService,
private readonly workspaceMigrationV2ViewFilterActionsBuilderService: WorkspaceMigrationV2ViewFilterActionsBuilderService,
private readonly workspaceMigrationV2ServerlessFunctionActionsBuilderService: WorkspaceMigrationV2ServerlessFunctionActionsBuilderService,
private readonly workspaceMigrationV2DatabaseEventTriggerActionsBuilderService: WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
private readonly workspaceMigrationV2CronTriggerActionsBuilderService: WorkspaceMigrationV2CronTriggerActionsBuilderService,
@@ -88,6 +90,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
cronTrigger: [],
routeTrigger: [],
fieldMetadata: [],
viewFilter: [],
};
const optimisticAllFlatEntityMaps = this.setupOptimisticCache({
@@ -104,6 +107,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatCronTriggerMaps,
flatRouteTriggerMaps,
flatFieldMetadataMaps,
flatViewFilterMaps,
} = fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
@@ -268,6 +272,36 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatViewFilterMaps)) {
const { from: fromFlatViewFilterMaps, to: toFlatViewFilterMaps } =
flatViewFilterMaps;
const viewFilterResult =
await this.workspaceMigrationV2ViewFilterActionsBuilderService.validateAndBuild(
{
from: fromFlatViewFilterMaps,
to: toFlatViewFilterMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatFieldMetadataMaps:
optimisticAllFlatEntityMaps.flatFieldMetadataMaps,
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
},
workspaceId,
},
);
optimisticAllFlatEntityMaps.flatViewFilterMaps =
viewFilterResult.optimisticFlatEntityMaps;
optimisticAllFlatEntityMaps.flatViewMaps =
viewFilterResult.dependencyOptimisticFlatEntityMaps.flatViewMaps;
if (viewFilterResult.status === 'fail') {
orchestratorFailureReport.viewFilter.push(...viewFilterResult.errors);
} else {
orchestratorActionsReport.viewFilter = viewFilterResult.actions;
}
}
if (isDefined(flatServerlessFunctionMaps)) {
const {
from: fromFlatServerlessFunctionMaps,
@@ -434,6 +468,9 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.viewField.deleted,
...aggregatedOrchestratorActionsReport.viewField.created,
...aggregatedOrchestratorActionsReport.viewField.updated,
...aggregatedOrchestratorActionsReport.viewFilter.deleted,
...aggregatedOrchestratorActionsReport.viewFilter.created,
...aggregatedOrchestratorActionsReport.viewFilter.updated,
///
// Serverless functions
@@ -0,0 +1,6 @@
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
export type ViewFilterRelatedFlatEntityMaps = Pick<
AllFlatEntityMaps,
'flatViewMaps' | 'flatFieldMetadataMaps'
>;
@@ -0,0 +1,197 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { compareTwoFlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/utils/compare-two-flat-view-filter.util';
import { ViewFilterRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/types/view-filter-related-flat-entity-maps.type';
import {
FlatEntityUpdateValidationArgs,
FlatEntityValidationArgs,
FlatEntityValidationReturnType,
WorkspaceEntityMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import {
UpdateViewFilterAction,
WorkspaceMigrationViewFilterActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-filter-action-v2.type';
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service';
@Injectable()
export class WorkspaceMigrationV2ViewFilterActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
'viewFilter',
FlatViewFilter,
WorkspaceMigrationViewFilterActionV2,
ViewFilterRelatedFlatEntityMaps
> {
constructor(
private readonly flatViewFilterValidatorService: FlatViewFilterValidatorService,
) {
super('viewFilter');
}
protected async validateFlatEntityCreation({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatViewFilterToValidate,
optimisticFlatEntityMaps: optimisticFlatViewFilterMaps,
}: FlatEntityValidationArgs<
FlatViewFilter,
ViewFilterRelatedFlatEntityMaps
>): Promise<
FlatEntityValidationReturnType<
WorkspaceMigrationViewFilterActionV2,
FlatViewFilter,
ViewFilterRelatedFlatEntityMaps
>
> {
const validationResult =
this.flatViewFilterValidatorService.validateFlatViewFilterCreation({
dependencyOptimisticFlatEntityMaps,
flatViewFilterToValidate,
optimisticFlatViewFilterMaps,
});
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const flatView = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFilterToValidate.viewId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
const updatedFlatViewMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatView,
viewFilterIds: [...flatView.viewFilterIds, flatViewFilterToValidate.id],
},
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
return {
status: 'success',
action: {
type: 'create_view_filter',
viewFilter: flatViewFilterToValidate,
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatViewMaps: updatedFlatViewMaps,
},
};
}
protected async validateFlatEntityDeletion({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatViewFilterToValidate,
optimisticFlatEntityMaps: optimisticFlatViewFilterMaps,
}: FlatEntityValidationArgs<
FlatViewFilter,
ViewFilterRelatedFlatEntityMaps
>): Promise<
FlatEntityValidationReturnType<
WorkspaceMigrationViewFilterActionV2,
FlatViewFilter,
ViewFilterRelatedFlatEntityMaps
>
> {
const validationResult =
this.flatViewFilterValidatorService.validateFlatViewFilterDeletion({
flatViewFilterToValidate,
optimisticFlatViewFilterMaps,
});
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.viewId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
const updatedFlatViewMaps = isDefined(flatView)
? replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatView,
viewFilterIds: flatView.viewFilterIds.filter(
(id) => id !== flatViewFilterToValidate.id,
),
},
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
})
: dependencyOptimisticFlatEntityMaps.flatViewMaps;
return {
status: 'success',
action: {
type: 'delete_view_filter',
viewFilterId: flatViewFilterToValidate.id,
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatViewMaps: updatedFlatViewMaps,
},
};
}
protected async validateFlatEntityUpdate({
dependencyOptimisticFlatEntityMaps,
flatEntityUpdate: { from: fromFlatViewFilter, to: toFlatViewFilter },
optimisticFlatEntityMaps: optimisticFlatViewFilterMaps,
}: FlatEntityUpdateValidationArgs<
FlatViewFilter,
ViewFilterRelatedFlatEntityMaps
>): Promise<
| FlatEntityValidationReturnType<
WorkspaceMigrationViewFilterActionV2,
FlatViewFilter,
ViewFilterRelatedFlatEntityMaps
>
| undefined
> {
const viewFilterUpdatedProperties = compareTwoFlatViewFilter({
fromFlatViewFilter,
toFlatViewFilter,
});
if (viewFilterUpdatedProperties.length === 0) {
return undefined;
}
const validationResult =
this.flatViewFilterValidatorService.validateFlatViewFilterUpdate({
dependencyOptimisticFlatEntityMaps,
flatViewFilterToValidate: toFlatViewFilter,
optimisticFlatViewFilterMaps,
});
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const updateViewFilterAction: UpdateViewFilterAction = {
type: 'update_view_filter',
viewFilterId: toFlatViewFilter.id,
updates: viewFilterUpdatedProperties,
};
return {
status: 'success',
action: updateViewFilterAction,
dependencyOptimisticFlatEntityMaps,
};
}
}
@@ -7,6 +7,7 @@ import { type WorkspaceMigrationRouteTriggerActionV2 } from 'src/engine/workspac
import { type WorkspaceMigrationServerlessFunctionActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type';
import { type WorkspaceMigrationViewActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
import { type WorkspaceMigrationViewFieldActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
import { type WorkspaceMigrationViewFilterActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-filter-action-v2.type';
export type WorkspaceMigrationActionV2 =
| WorkspaceMigrationObjectActionV2
@@ -14,6 +15,7 @@ export type WorkspaceMigrationActionV2 =
| WorkspaceMigrationIndexActionV2
| WorkspaceMigrationViewActionV2
| WorkspaceMigrationViewFieldActionV2
| WorkspaceMigrationViewFilterActionV2
| WorkspaceMigrationServerlessFunctionActionV2
| WorkspaceMigrationDatabaseEventTriggerActionV2
| WorkspaceMigrationCronTriggerActionV2
@@ -1,30 +1,20 @@
import { type FromTo } from 'twenty-shared/types';
import { type FlatViewFieldPropertiesToCompare } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-properties-to-compare.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
export type FlatViewFieldPropertyUpdate<
P extends FlatViewFieldPropertiesToCompare,
> = {
property: P;
} & FromTo<ViewFieldEntity[P]>;
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
export type CreateViewFieldAction = {
type: 'create_view_field';
viewField: FlatViewField;
};
export type FlatViewFieldPropertiesUpdates = Array<
{
[P in FlatViewFieldPropertiesToCompare]: FlatViewFieldPropertyUpdate<P>;
}[FlatViewFieldPropertiesToCompare]
>;
export type UpdateViewFieldAction = {
type: 'update_view_field';
viewFieldId: string;
updates: FlatViewFieldPropertiesUpdates;
updates: Array<
{
[P in FlatViewFieldPropertiesToCompare]: PropertyUpdate<FlatViewField, P>;
}[FlatViewFieldPropertiesToCompare]
>;
};
export type DeleteViewFieldAction = {
@@ -0,0 +1,34 @@
import { type FlatViewFilterPropertiesToCompare } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-properties-to-compare.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
export type CreateViewFilterAction = {
type: 'create_view_filter';
viewFilter: FlatViewFilter;
};
export type UpdateViewFilterAction = {
type: 'update_view_filter';
viewFilterId: string;
updates: Array<
{
[P in FlatViewFilterPropertiesToCompare]: PropertyUpdate<
FlatViewFilter,
P
>;
}[FlatViewFilterPropertiesToCompare]
>;
};
export type DeleteViewFilterAction = {
type: 'delete_view_filter';
viewFilterId: string;
};
export type WorkspaceMigrationViewFilterActionV2 =
| CreateViewFilterAction
| UpdateViewFilterAction
| DeleteViewFilterAction;
export type WorkspaceMigrationViewFilterActionTypeV2 =
WorkspaceMigrationViewFilterActionV2['type'];
@@ -0,0 +1,158 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { ViewFilterExceptionCode } from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { ViewFilterRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/types/view-filter-related-flat-entity-maps.type';
@Injectable()
export class FlatViewFilterValidatorService {
constructor() {}
validateFlatViewFilterCreation({
flatViewFilterToValidate,
dependencyOptimisticFlatEntityMaps,
optimisticFlatViewFilterMaps,
}: {
flatViewFilterToValidate: FlatViewFilter;
dependencyOptimisticFlatEntityMaps: ViewFilterRelatedFlatEntityMaps;
optimisticFlatViewFilterMaps: FlatViewFilterMaps;
}): FailedFlatEntityValidation<FlatViewFilter> {
const validationResult: FailedFlatEntityValidation<FlatViewFilter> = {
type: 'create_view_filter',
errors: [],
flatEntityMinimalInformation: {
id: flatViewFilterToValidate.id,
},
};
const existingViewFilter = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.id,
flatEntityMaps: optimisticFlatViewFilterMaps,
});
if (isDefined(existingViewFilter)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
message: t`View filter with this id already exists`,
userFriendlyMessage: msg`View filter with this id already exists`,
});
}
const referencedView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.viewId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
if (!isDefined(referencedView)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
message: t`View not found`,
userFriendlyMessage: msg`View not found`,
});
}
const referencedFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
if (!isDefined(referencedFieldMetadata)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
message: t`Field metadata not found`,
userFriendlyMessage: msg`Field metadata not found`,
});
}
return validationResult;
}
validateFlatViewFilterDeletion({
flatViewFilterToValidate,
optimisticFlatViewFilterMaps,
}: {
flatViewFilterToValidate: FlatViewFilter;
optimisticFlatViewFilterMaps: FlatViewFilterMaps;
}): FailedFlatEntityValidation<FlatViewFilter> {
const validationResult: FailedFlatEntityValidation<FlatViewFilter> = {
type: 'delete_view_filter',
errors: [],
flatEntityMinimalInformation: {
id: flatViewFilterToValidate.id,
},
};
const existingViewFilter = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.id,
flatEntityMaps: optimisticFlatViewFilterMaps,
});
if (!isDefined(existingViewFilter)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
message: t`View filter not found`,
userFriendlyMessage: msg`View filter not found`,
});
} else if (!isDefined(existingViewFilter.deletedAt)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
message: t`View filter has to be soft deleted first`,
userFriendlyMessage: msg`View filter has to be soft deleted first`,
});
}
return validationResult;
}
validateFlatViewFilterUpdate({
flatViewFilterToValidate,
dependencyOptimisticFlatEntityMaps,
optimisticFlatViewFilterMaps,
}: {
flatViewFilterToValidate: FlatViewFilter;
dependencyOptimisticFlatEntityMaps: ViewFilterRelatedFlatEntityMaps;
optimisticFlatViewFilterMaps: FlatViewFilterMaps;
}): FailedFlatEntityValidation<FlatViewFilter> {
const validationResult: FailedFlatEntityValidation<FlatViewFilter> = {
type: 'update_view_filter',
errors: [],
flatEntityMinimalInformation: {
id: flatViewFilterToValidate.id,
},
};
const existingViewFilter = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.id,
flatEntityMaps: optimisticFlatViewFilterMaps,
});
if (!isDefined(existingViewFilter)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
message: t`View filter not found`,
userFriendlyMessage: msg`View filter not found`,
});
}
const referencedFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFilterToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
if (!isDefined(referencedFieldMetadata)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
message: t`Field metadata not found`,
userFriendlyMessage: msg`Field metadata not found`,
});
}
return validationResult;
}
}
@@ -10,6 +10,7 @@ import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager
import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-route-trigger-validator.service';
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-serverless-function-validator.service';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service';
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service';
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service';
@Module({
@@ -17,6 +18,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
providers: [
FlatViewValidatorService,
FlatViewFieldValidatorService,
FlatViewFilterValidatorService,
FlatIndexValidatorService,
FlatFieldMetadataValidatorService,
FlatObjectMetadataValidatorService,
@@ -29,6 +31,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
exports: [
FlatViewValidatorService,
FlatViewFieldValidatorService,
FlatViewFilterValidatorService,
FlatIndexValidatorService,
FlatFieldMetadataValidatorService,
FlatObjectMetadataValidatorService,
@@ -10,6 +10,7 @@ import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/work
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
import { WorkspaceMigrationV2ViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service';
import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module';
@@ -20,6 +21,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2ObjectActionsBuilderService,
WorkspaceMigrationV2ViewActionsBuilderService,
WorkspaceMigrationV2ViewFieldActionsBuilderService,
WorkspaceMigrationV2ViewFilterActionsBuilderService,
WorkspaceMigrationV2IndexActionsBuilderService,
WorkspaceMigrationV2ServerlessFunctionActionsBuilderService,
WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
@@ -32,6 +34,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2IndexActionsBuilderService,
WorkspaceMigrationV2ObjectActionsBuilderService,
WorkspaceMigrationV2ViewFieldActionsBuilderService,
WorkspaceMigrationV2ViewFilterActionsBuilderService,
WorkspaceMigrationV2FieldActionsBuilderService,
WorkspaceMigrationV2ServerlessFunctionActionsBuilderService,
WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { CreateViewFilterAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-filter-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_view_filter',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateViewFilterAction>): Partial<AllFlatEntityMaps> {
const { flatViewFilterMaps } = allFlatEntityMaps;
const { viewFilter } = action;
const updatedFlatViewFilterMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: viewFilter,
flatEntityMaps: flatViewFilterMaps,
});
return {
flatViewFilterMaps: updatedFlatViewFilterMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateViewFilterAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { viewFilter } = action;
const viewFilterRepository =
queryRunner.manager.getRepository<ViewFilterEntity>(ViewFilterEntity);
await viewFilterRepository.insert({
...viewFilter,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateViewFilterAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { DeleteViewFilterAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-filter-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteViewFilterActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_view_filter',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteViewFilterAction>): Partial<AllFlatEntityMaps> {
const { flatViewFilterMaps } = allFlatEntityMaps;
const { viewFilterId } = action;
const updatedFlatViewFilterMaps = deleteFlatEntityFromFlatEntityMapsOrThrow(
{
entityToDeleteId: viewFilterId,
flatEntityMaps: flatViewFilterMaps,
},
);
return {
flatViewFilterMaps: updatedFlatViewFilterMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteViewFilterAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { viewFilterId } = action;
const viewFilterRepository =
queryRunner.manager.getRepository<ViewFilterEntity>(ViewFilterEntity);
await viewFilterRepository.delete({
id: viewFilterId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteViewFilterAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { UpdateViewFilterAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-filter-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util';
@Injectable()
export class UpdateViewFilterActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_view_filter',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateViewFilterAction>): Partial<AllFlatEntityMaps> {
const { flatViewFilterMaps } = allFlatEntityMaps;
const { viewFilterId } = action;
const existingViewFilter = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: viewFilterId,
flatEntityMaps: flatViewFilterMaps,
});
const updatedViewFilter = {
...existingViewFilter,
...fromWorkspaceMigrationUpdateActionToPartialEntity(action),
};
const updatedFlatViewFilterMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedViewFilter,
flatEntityMaps: flatViewFilterMaps,
});
return {
flatViewFilterMaps: updatedFlatViewFilterMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateViewFilterAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { viewFilterId } = action;
const viewFilterRepository =
queryRunner.manager.getRepository<ViewFilterEntity>(ViewFilterEntity);
const update = fromWorkspaceMigrationUpdateActionToPartialEntity(action);
await viewFilterRepository.update(viewFilterId, update);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateViewFilterAction>,
): Promise<void> {
return;
}
}
@@ -24,6 +24,9 @@ import { UpdateServerlessFunctionActionHandlerService } from 'src/engine/workspa
import { CreateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/create-view-field-action-handler.service';
import { DeleteViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/delete-view-field-action-handler.service';
import { UpdateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/update-view-field-action-handler.service';
import { CreateViewFilterActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter/services/create-view-filter-action-handler.service';
import { DeleteViewFilterActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter/services/delete-view-filter-action-handler.service';
import { UpdateViewFilterActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter/services/update-view-filter-action-handler.service';
import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/create-view-action-handler.service';
import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/delete-view-action-handler.service';
import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/update-view-action-handler.service';
@@ -50,6 +53,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
UpdateViewFieldActionHandlerService,
DeleteViewFieldActionHandlerService,
CreateViewFilterActionHandlerService,
UpdateViewFilterActionHandlerService,
DeleteViewFilterActionHandlerService,
CreateServerlessFunctionActionHandlerService,
DeleteServerlessFunctionActionHandlerService,
UpdateServerlessFunctionActionHandlerService,
@@ -1,7 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function orderObjectProperties<T extends Array<any> | object>(
data: T,
): T {
export function orderObjectProperties<T>(data: T): T {
if (Array.isArray(data)) {
return data.map(orderObjectProperties) as T;
}