[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
@@ -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 {}