Add workspace migration orchestrator + refactor builders + view/viewField action builders (#14383)

This commit is contained in:
Weiko
2025-09-10 16:55:30 +02:00
committed by GitHub
parent fcf3dc4429
commit 61f1a883a0
78 changed files with 2557 additions and 468 deletions
@@ -0,0 +1,14 @@
import { CustomException } from 'src/utils/custom-exception';
export class FlatEntityException extends CustomException {
code: FlatEntityExceptionCode;
constructor(message: string, code: FlatEntityExceptionCode) {
super(message, code);
}
}
export enum FlatEntityExceptionCode {
ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
}
@@ -0,0 +1,6 @@
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
export type FlatEntityMaps<T extends FlatEntity> = {
byId: Partial<Record<string, T>>;
idByUniversalIdentifier: Partial<Record<string, string>>;
};
@@ -0,0 +1,4 @@
export interface FlatEntity {
id: string;
universalIdentifier: string;
}
@@ -0,0 +1,36 @@
import { isDefined } from 'class-validator';
import {
FlatEntityException,
FlatEntityExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity.exception';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
type AddFlatEntityToFlatEntityMapsOrThrowArgs<T extends FlatEntity> = {
flatEntity: T;
flatEntityMaps: FlatEntityMaps<T>;
};
export const addFlatEntityToFlatEntityMapsOrThrow = <T extends FlatEntity>({
flatEntity,
flatEntityMaps,
}: AddFlatEntityToFlatEntityMapsOrThrowArgs<T>): FlatEntityMaps<T> => {
if (isDefined(flatEntityMaps.byId[flatEntity.id])) {
throw new FlatEntityException(
'addFlatEntityToFlatEntityMapsOrThrow: flat entity to add already exists',
FlatEntityExceptionCode.ENTITY_ALREADY_EXISTS,
);
}
return {
byId: {
...flatEntityMaps.byId,
[flatEntity.id]: flatEntity,
},
idByUniversalIdentifier: {
...flatEntityMaps.idByUniversalIdentifier,
[flatEntity.universalIdentifier]: flatEntity.id,
},
};
};
@@ -0,0 +1,40 @@
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import {
FlatEntityException,
FlatEntityExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity.exception';
export type DeleteFlatEntityFromFlatEntityMapsOrThrowArgs<
T extends FlatEntity,
> = {
entityToDeleteId: string;
flatEntityMaps: FlatEntityMaps<T>;
};
export const deleteFlatEntityFromFlatEntityMapsOrThrow = <
T extends FlatEntity,
>({
flatEntityMaps,
entityToDeleteId,
}: DeleteFlatEntityFromFlatEntityMapsOrThrowArgs<T>): FlatEntityMaps<T> => {
if (!isDefined(flatEntityMaps.byId[entityToDeleteId])) {
throw new FlatEntityException(
'deleteFlatEntityFromFlatEntityMapsOrThrow: entity to delete not found',
FlatEntityExceptionCode.ENTITY_NOT_FOUND,
);
}
const updatedIdByUniversalIdentifierEntries = Object.entries(
flatEntityMaps.idByUniversalIdentifier,
).filter(([_universalIdentifier, id]) => id !== entityToDeleteId);
return {
byId: removePropertiesFromRecord(flatEntityMaps.byId, [entityToDeleteId]),
idByUniversalIdentifier: Object.fromEntries(
updatedIdByUniversalIdentifierEntries,
),
};
};
@@ -0,0 +1,25 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
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';
export type ReplaceFlatEntityInFlatEntityMapsOrThrowArgs<T extends FlatEntity> =
{
flatEntity: T;
flatEntityMaps: FlatEntityMaps<T>;
};
export const replaceFlatEntityInFlatEntityMapsOrThrow = <T extends FlatEntity>({
flatEntity,
flatEntityMaps,
}: ReplaceFlatEntityInFlatEntityMapsOrThrowArgs<T>): FlatEntityMaps<T> => {
const flatEntityMapsToReplace = deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps,
entityToDeleteId: flatEntity.id,
});
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity,
flatEntityMaps: flatEntityMapsToReplace,
});
};
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
@Module({
imports: [TypeOrmModule.forFeature([ViewEntity])],
providers: [ViewCacheService],
exports: [ViewCacheService],
})
export class ViewCacheModule {}
@@ -0,0 +1,47 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { generateFlatViewMaps } from 'src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util';
type GetExistingOrRecomputeFlatViewMapsResult = {
flatViewMaps: FlatViewMaps;
};
@Injectable()
export class ViewCacheService {
logger = new Logger(ViewCacheService.name);
constructor(
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {}
async getExistingOrRecomputeFlatViewMaps({
workspaceId,
}: {
workspaceId: string;
}): Promise<GetExistingOrRecomputeFlatViewMapsResult> {
// TODO: get from cache later
const existingViews = await this.viewRepository.find({
where: {
workspaceId,
},
relations: ['viewFields'],
select: {
viewFields: {
id: true,
},
},
});
const existingFlatViewMaps = generateFlatViewMaps(existingViews);
return {
flatViewMaps: existingFlatViewMaps,
};
}
}
@@ -31,7 +31,10 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
where: '"deletedAt" IS NULL',
},
)
export class ViewFieldEntity extends SyncableEntity {
export class ViewFieldEntity
extends SyncableEntity
implements Required<ViewFieldEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -59,7 +62,7 @@ export class ViewFieldEntity extends SyncableEntity {
nullable: true,
default: null,
})
aggregateOperation?: AggregateOperations | null;
aggregateOperation: AggregateOperations | null;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@@ -74,7 +77,7 @@ export class ViewFieldEntity extends SyncableEntity {
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
deletedAt: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
@@ -31,7 +31,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
'workspaceId',
'objectMetadataId',
])
export class ViewEntity extends SyncableEntity {
export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -89,10 +89,10 @@ export class ViewEntity extends SyncableEntity {
nullable: true,
default: null,
})
kanbanAggregateOperation?: AggregateOperations | null;
kanbanAggregateOperation: AggregateOperations | null;
@Column({ nullable: true, type: 'uuid' })
kanbanAggregateOperationFieldMetadataId?: string | null;
kanbanAggregateOperationFieldMetadataId: string | null;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -104,10 +104,10 @@ export class ViewEntity extends SyncableEntity {
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
deletedAt: Date | null;
@Column({ nullable: true, type: 'text', default: null })
anyFieldFilterValue?: string | null;
anyFieldFilterValue: string | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
@@ -0,0 +1,13 @@
import {
appendCommonExceptionCode,
CustomException,
} from 'src/utils/custom-exception';
export class FlatViewException extends CustomException<
keyof typeof FlatViewExceptionCode
> {}
export const FlatViewExceptionCode = appendCommonExceptionCode({
VIEW_NOT_FOUND: 'VIEW_NOT_FOUND',
VIEW_ALREADY_EXISTS: 'VIEW_ALREADY_EXISTS',
} as const);
@@ -0,0 +1,8 @@
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE = [
'isVisible',
'size',
'position',
'aggregateOperation',
] as const satisfies (keyof FlatViewField)[];
@@ -0,0 +1,14 @@
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export const FLAT_VIEW_PROPERTIES_TO_COMPARE = [
'name',
'type',
'key',
'isCompact',
'openRecordIn',
'kanbanAggregateOperation',
'kanbanAggregateOperationFieldMetadataId',
'position',
'anyFieldFilterValue',
'icon',
] as const satisfies (keyof FlatView)[];
@@ -0,0 +1,11 @@
import { type ViewEntityRelationProperties } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export const VIEW_ENTITY_RELATION_PROPERTIES = [
'objectMetadata',
'viewFields',
'viewFilters',
'viewFilterGroups',
'viewGroups',
'viewSorts',
'workspace',
] as const satisfies ViewEntityRelationProperties[];
@@ -0,0 +1,7 @@
import { type ViewFieldEntityRelationProperties } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const VIEW_FIELD_ENTITY_RELATION_PROPERTIES = [
'fieldMetadata',
'view',
'workspace',
] as const satisfies ViewFieldEntityRelationProperties[];
@@ -0,0 +1,5 @@
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export type FailedFlatViewFieldValidationMinimalInformation = Partial<
Pick<FlatViewField, 'id' | 'fieldMetadataId' | 'viewId'>
>;
@@ -0,0 +1,9 @@
import { type FailedFlatViewFieldValidationMinimalInformation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation-minimal-information.type';
import { type FlatViewFieldValidationError } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-validation-error.type';
import { type WorkspaceMigrationViewFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
export type FailedFlatViewFieldValidation = {
type: WorkspaceMigrationViewFieldActionTypeV2;
viewFieldLevelErrors: FlatViewFieldValidationError[];
failedViewFieldValidationMinimalInformation: Partial<FailedFlatViewFieldValidationMinimalInformation>;
};
@@ -0,0 +1,5 @@
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export type FailedFlatViewValidationMinimalInformation = Partial<
Pick<FlatView, 'id' | 'name' | 'objectMetadataId'>
>;
@@ -0,0 +1,9 @@
import { type FailedFlatViewValidationMinimalInformation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation-minimal-information.type';
import { type FlatViewValidationError } from 'src/engine/core-modules/view/flat-view/types/flat-view-validation-error.type';
import { type WorkspaceMigrationViewActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
export type FailedFlatViewValidation = {
type: WorkspaceMigrationViewActionTypeV2;
viewLevelErrors: FlatViewValidationError[];
failedViewValidationMinimalInformation: Partial<FailedFlatViewValidationMinimalInformation>;
};
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export type FlatViewFieldMaps = FlatEntityMaps<FlatViewField>;
@@ -0,0 +1,4 @@
import { type FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant';
export type FlatViewFieldPropertiesToCompare =
(typeof FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,4 @@
export type FlatViewFieldValidationError = {
message: string;
code: string;
};
@@ -0,0 +1,18 @@
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
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 ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type ViewFieldEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ViewFieldEntity,
FieldMetadataEntity | ViewEntity | Workspace
>;
export type FlatViewField = Omit<
ViewFieldEntity,
ViewFieldEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export type FlatViewMaps = FlatEntityMaps<FlatView>;
@@ -0,0 +1,4 @@
import { type FLAT_VIEW_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant';
export type FlatViewPropertiesToCompare =
(typeof FLAT_VIEW_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,8 @@
import { type ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception';
export type FlatViewValidationError = {
code: ViewExceptionCode;
message: string;
userFriendlyMessage?: string;
value?: unknown;
};
@@ -0,0 +1,26 @@
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { type ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { type ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { type ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity';
import { type ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type ViewEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ViewEntity,
| ObjectMetadataEntity
| ViewFieldEntity
| ViewSortEntity
| ViewFilterEntity
| ViewGroupEntity
| ViewFilterGroupEntity
| Workspace
>;
export type FlatView = Omit<ViewEntity, ViewEntityRelationProperties> & {
universalIdentifier: string;
viewFieldIds: string[];
};
@@ -0,0 +1,59 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant';
import { type FlatViewFieldPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/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,
'FlatViewField'
>;
export const compareTwoFlatViewField = ({
fromFlatViewField,
toFlatViewField,
}: GetWorkspaceMigrationUpdateViewFieldActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE.includes(
property as FlatViewFieldPropertiesToCompare,
),
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 [];
}
}
});
};
@@ -0,0 +1,56 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { FLAT_VIEW_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant';
import { type FlatViewPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import { type UpdateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type GetWorkspaceMigrationUpdateViewActionArgs = FromTo<FlatView, 'FlatView'>;
export const compareTwoFlatView = ({
fromFlatView,
toFlatView,
}: GetWorkspaceMigrationUpdateViewActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_VIEW_PROPERTIES_TO_COMPARE.includes(
property as FlatViewPropertiesToCompare,
),
propertiesToStringify: [],
};
const fromCompare = transformMetadataForComparison(
fromFlatView,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
toFlatView,
transformMetadataForComparisonParameters,
);
const flatViewDifferences = diff(fromCompare, toCompare);
return flatViewDifferences.flatMap<UpdateViewAction['updates'][number]>(
(difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatViewPropertiesToCompare;
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,27 @@
import { v4 } from 'uuid';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const fromPartialFlatViewFieldToFlatViewFieldWithDefault = (
partialFlatViewField: Partial<FlatViewField>,
): FlatViewField => {
const createdAt = new Date();
const viewFieldId = partialFlatViewField.id ?? v4();
return {
...partialFlatViewField,
id: viewFieldId,
fieldMetadataId: partialFlatViewField.fieldMetadataId ?? '',
isVisible: partialFlatViewField.isVisible ?? true,
size: partialFlatViewField.size ?? 0,
position: partialFlatViewField.position ?? 0,
aggregateOperation: partialFlatViewField.aggregateOperation ?? null,
viewId: partialFlatViewField.viewId ?? '',
workspaceId: partialFlatViewField.workspaceId ?? '',
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier:
partialFlatViewField.universalIdentifier ?? viewFieldId,
};
};
@@ -0,0 +1,36 @@
import { v4 } from 'uuid';
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
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 ?? [],
};
};
@@ -0,0 +1,20 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { VIEW_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => {
const viewEntityWithoutRelations = removePropertiesFromRecord(
viewEntity,
VIEW_ENTITY_RELATION_PROPERTIES,
);
return {
...viewEntityWithoutRelations,
universalIdentifier:
viewEntityWithoutRelations.universalIdentifier ??
viewEntityWithoutRelations.id,
viewFieldIds: viewEntity.viewFields?.map((viewField) => viewField.id) ?? [],
};
};
@@ -0,0 +1,21 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const fromViewFieldEntityToFlatViewField = (
viewFieldEntity: ViewFieldEntity,
): FlatViewField => {
const viewFieldEntityWithoutRelations = removePropertiesFromRecord(
viewFieldEntity,
VIEW_FIELD_ENTITY_RELATION_PROPERTIES,
);
return {
...viewFieldEntityWithoutRelations,
universalIdentifier:
viewFieldEntityWithoutRelations.universalIdentifier ??
viewFieldEntityWithoutRelations.id,
};
};
@@ -0,0 +1,27 @@
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export const generateFlatViewMaps = (
viewCollection: ViewEntity[],
): FlatViewMaps => {
const flatViewMaps: FlatViewMaps = {
byId: {},
idByUniversalIdentifier: {},
};
for (const view of viewCollection) {
const processedFlatView: FlatView = {
...view,
viewFieldIds: view.viewFields?.map((viewField) => viewField.id) ?? [],
universalIdentifier: view.universalIdentifier ?? view.id, // TODO: should not fallback to id, to remove once we have universalIdentifier required
};
flatViewMaps.byId[view.id] = processedFlatView;
flatViewMaps.idByUniversalIdentifier[
processedFlatView.universalIdentifier
] = view.id;
}
return flatViewMaps;
};
@@ -0,0 +1,244 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { Equal, Repository } from 'typeorm';
import { v4 } from 'uuid';
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 { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
ViewFieldExceptionMessageKey,
generateViewFieldExceptionMessage,
generateViewFieldUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { fromPartialFlatViewFieldToFlatViewFieldWithDefault } from 'src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util';
import { fromViewFieldEntityToFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util';
import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception';
import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service';
@Injectable()
export class ViewFieldV2Service {
constructor(
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
private readonly workspaceMigrationOrchestratorService: WorkspaceMigrationBuildOrchestratorService,
) {}
// TODO: move to cache service
private async getExistingFlatViewFieldMaps(
workspaceId: string,
): Promise<FlatViewFieldMaps> {
const existingViewFields = await this.viewFieldRepository.find({
where: { workspaceId },
});
const flatViewFieldMaps: FlatViewFieldMaps = {
byId: {},
idByUniversalIdentifier: {},
};
for (const viewFieldEntity of existingViewFields) {
const flatViewField = fromViewFieldEntityToFlatViewField(viewFieldEntity);
flatViewFieldMaps.byId[flatViewField.id] = flatViewField;
flatViewFieldMaps.idByUniversalIdentifier[
flatViewField.universalIdentifier
] = flatViewField.id;
}
return flatViewFieldMaps;
}
async createOne(
viewFieldData: Partial<ViewFieldEntity>,
): Promise<ViewFieldEntity> {
if (!isDefined(viewFieldData.workspaceId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
const existingFlatViewFieldMaps = await this.getExistingFlatViewFieldMaps(
viewFieldData.workspaceId,
);
const flatViewFieldFromCreateInput =
fromPartialFlatViewFieldToFlatViewFieldWithDefault({
...viewFieldData,
universalIdentifier: viewFieldData.universalIdentifier ?? v4(),
});
const toFlatViewFieldMaps: FlatViewFieldMaps =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFieldFromCreateInput,
flatEntityMaps: existingFlatViewFieldMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId: viewFieldData.workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while creating view field',
);
}
const [createdViewField] = await this.viewFieldRepository.find({
where: {
id: flatViewFieldFromCreateInput.id,
},
});
return createdViewField;
}
async updateOne(
id: string,
workspaceId: string,
updateData: Partial<ViewFieldEntity>,
): Promise<ViewFieldEntity> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMaps(workspaceId);
const existingViewField = existingFlatViewFieldMaps.byId[id];
if (!isDefined(existingViewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const existingViewFieldToUpdate = removePropertiesFromRecord(
{
...existingViewField,
...updateData,
},
VIEW_FIELD_ENTITY_RELATION_PROPERTIES,
);
const flatViewFieldFromUpdateInput =
fromPartialFlatViewFieldToFlatViewFieldWithDefault({
...existingViewFieldToUpdate,
universalIdentifier:
existingViewFieldToUpdate.universalIdentifier ?? '',
});
const toFlatViewFieldMaps: FlatViewFieldMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatViewFieldFromUpdateInput,
flatEntityMaps: existingFlatViewFieldMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while updating view field',
);
}
const [updatedViewField] = await this.viewFieldRepository.find({
where: {
id: Equal(id),
},
});
return updatedViewField;
}
async deleteOne(id: string, workspaceId: string): Promise<boolean> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMaps(workspaceId);
const existingViewFieldToDelete = existingFlatViewFieldMaps.byId[id];
if (!isDefined(existingViewFieldToDelete)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const toFlatViewFieldMaps: FlatViewFieldMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: existingFlatViewFieldMaps,
entityToDeleteId: existingViewFieldToDelete.id,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while deleting view field',
);
}
return true;
}
}
@@ -0,0 +1,219 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { Equal, Repository } from 'typeorm';
import { v4 } from 'uuid';
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 { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import {
ViewException,
ViewExceptionCode,
ViewExceptionMessageKey,
generateViewExceptionMessage,
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { VIEW_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { fromPartialFlatViewToFlatViewWithDefault } from 'src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util';
import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception';
import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service';
@Injectable()
export class ViewV2Service {
constructor(
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly workspaceMigrationOrchestratorService: WorkspaceMigrationBuildOrchestratorService,
private readonly viewCacheService: ViewCacheService,
) {}
async createOne(viewData: Partial<ViewEntity>): Promise<ViewEntity> {
if (!isDefined(viewData.workspaceId)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewExceptionCode.INVALID_VIEW_DATA,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
const { flatViewMaps: existingFlatViewMaps } =
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId: viewData.workspaceId,
});
const flatViewFromCreateInput = fromPartialFlatViewToFlatViewWithDefault({
...viewData,
universalIdentifier: viewData.universalIdentifier ?? v4(),
});
const toFlatViewMaps: FlatViewMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFromCreateInput,
flatEntityMaps: existingFlatViewMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
view: {
fromFlatViewMaps: existingFlatViewMaps,
toFlatViewMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId: viewData.workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while creating view',
);
}
const [createdView] = await this.viewRepository.find({
where: {
id: flatViewFromCreateInput.id,
},
});
return createdView;
}
async updateOne(
id: string,
workspaceId: string,
updateData: Partial<ViewEntity>,
): Promise<ViewEntity> {
const { flatViewMaps: existingFlatViewMaps } =
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId,
});
const existingView = existingFlatViewMaps.byId[id];
if (!isDefined(existingView)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_NOT_FOUND,
id,
),
ViewExceptionCode.VIEW_NOT_FOUND,
);
}
const existingViewToUpdate = removePropertiesFromRecord(
{
...existingView,
...updateData,
},
VIEW_ENTITY_RELATION_PROPERTIES,
);
const flatViewFromUpdateInput = fromPartialFlatViewToFlatViewWithDefault({
...existingViewToUpdate,
universalIdentifier: existingViewToUpdate.universalIdentifier ?? '',
});
const toFlatViewMaps: FlatViewMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatViewFromUpdateInput,
flatEntityMaps: existingFlatViewMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
view: {
fromFlatViewMaps: existingFlatViewMaps,
toFlatViewMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while updating view',
);
}
const [updatedView] = await this.viewRepository.find({
where: {
id: Equal(id),
},
});
return updatedView;
}
async deleteOne(id: string, workspaceId: string): Promise<boolean> {
const { flatViewMaps: existingFlatViewMaps } =
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId,
});
const existingViewToDelete = existingFlatViewMaps.byId[id];
if (!isDefined(existingViewToDelete)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_NOT_FOUND,
id,
),
ViewExceptionCode.VIEW_NOT_FOUND,
);
}
const toFlatViewMaps: FlatViewMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: existingFlatViewMaps,
entityToDeleteId: existingViewToDelete.id,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
view: {
fromFlatViewMaps: existingFlatViewMaps,
toFlatViewMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while deleting view',
);
}
return true;
}
}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
import { ViewCacheModule } from 'src/engine/core-modules/view/cache/services/view-cache.module';
import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller';
import { ViewFilterGroupController } from 'src/engine/core-modules/view/controllers/view-filter-group.controller';
import { ViewFilterController } from 'src/engine/core-modules/view/controllers/view-filter.controller';
@@ -20,14 +21,17 @@ import { ViewFilterResolver } from 'src/engine/core-modules/view/resolvers/view-
import { ViewGroupResolver } from 'src/engine/core-modules/view/resolvers/view-group.resolver';
import { ViewSortResolver } from 'src/engine/core-modules/view/resolvers/view-sort.resolver';
import { ViewResolver } from 'src/engine/core-modules/view/resolvers/view.resolver';
import { ViewFieldV2Service } from 'src/engine/core-modules/view/services/view-field-v2.service';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewV2Service } from 'src/engine/core-modules/view/services/view-v2.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
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: [
@@ -42,6 +46,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
I18nModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
WorkspaceMigrationV2Module,
ViewCacheModule,
],
controllers: [
ViewController,
@@ -64,6 +70,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
ViewFilterGroupResolver,
ViewGroupResolver,
ViewSortResolver,
ViewV2Service,
ViewFieldV2Service,
],
exports: [
ViewService,
@@ -72,6 +80,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
ViewFilterGroupService,
ViewGroupService,
ViewSortService,
ViewV2Service,
ViewFieldV2Service,
],
})
export class CoreViewModule {}
@@ -105,7 +105,7 @@ export class FieldMetadataServiceV2 {
{
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: true,
inferDeletionFromMissingEntities: true,
},
fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject,
toFlatObjectMetadataMaps,
@@ -181,7 +181,7 @@ export class FieldMetadataServiceV2 {
toFlatObjectMetadataMaps,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
@@ -270,7 +270,7 @@ export class FieldMetadataServiceV2 {
toFlatObjectMetadataMaps,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
@@ -5,7 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
import { ViewV2Service } from 'src/engine/core-modules/view/services/view-v2.service';
import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util';
import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-field-from-flat-object-metadata-maps-or-throw.util';
@@ -42,6 +42,7 @@ export class ObjectMetadataServiceV2 {
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly viewV2Service: ViewV2Service,
) {}
async updateOne({
@@ -96,7 +97,7 @@ export class ObjectMetadataServiceV2 {
toFlatObjectMetadataMaps,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
@@ -198,7 +199,7 @@ export class ObjectMetadataServiceV2 {
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
buildOptions: {
inferDeletionFromMissingObjectFieldIndex: true,
inferDeletionFromMissingEntities: true,
isSystemBuild: false,
},
workspaceId,
@@ -288,7 +289,7 @@ export class ObjectMetadataServiceV2 {
toFlatObjectMetadataMaps,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
@@ -318,26 +319,6 @@ export class ObjectMetadataServiceV2 {
);
}
const [view] = await this.viewRepository.find({
where: {
objectMetadataId: createdFlatObjectMetadata.id,
key: ViewKey.INDEX,
workspaceId,
},
});
if (!isDefined(view)) {
throw new ObjectMetadataException(
'View not created',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
await this.objectMetadataRelatedRecordsService.createViewWorkspaceFavorite(
workspaceId,
view.id,
);
return createdFlatObjectMetadata;
}
}
@@ -0,0 +1,6 @@
export class WorkspaceMigrationOrchestratorException extends Error {
constructor(message = 'Workspace migration orchestrator failed') {
super(message);
this.name = 'WorkspaceMigrationOrchestratorException';
}
}
@@ -0,0 +1,189 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception';
import {
WorkspaceMigrationOrchestratorBuildArgs,
WorkspaceMigrationOrchestratorEntityMaps,
WorkspaceMigrationOrchestratorFailedResult,
WorkspaceMigrationOrchestratorOptimisticEntityMaps,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
import { WorkspaceViewFieldMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service';
import { WorkspaceViewMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service';
import {
WorkspaceMigrationV2Exception,
WorkspaceMigrationV2ExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration.exception';
@Injectable()
export class WorkspaceMigrationBuildOrchestratorService {
private readonly logger = new Logger(
WorkspaceMigrationBuildOrchestratorService.name,
);
constructor(
private readonly workspaceMigrationBuilderV2Service: WorkspaceMigrationBuilderV2Service,
private readonly workspaceViewMigrationBuilderV2Service: WorkspaceViewMigrationBuilderV2Service,
private readonly workspaceViewFieldMigrationBuilderV2Service: WorkspaceViewFieldMigrationBuilderV2Service,
private readonly migrationRunner: WorkspaceMigrationRunnerV2Service,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly viewCacheService: ViewCacheService,
) {}
public async buildWorkspaceMigrations({
workspaceId,
buildOptions,
entityMaps,
}: WorkspaceMigrationOrchestratorBuildArgs): Promise<
WorkspaceMigrationOrchestratorFailedResult | undefined
> {
try {
const allObjectAndFieldActions = [];
const allViewActions = [];
const allViewFieldActions = [];
const allFailures = [];
const optimisticEntityMaps = await this.prepareOptimisticEntityMaps({
entityMaps,
workspaceId,
});
if (entityMaps.object) {
const objectResult =
await this.workspaceMigrationBuilderV2Service.validateAndBuild({
fromFlatObjectMetadataMaps:
entityMaps.object.fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps:
entityMaps.object.toFlatObjectMetadataMaps,
workspaceId,
buildOptions,
});
if (objectResult.status === 'fail') {
allFailures.push(...objectResult.errors);
} else {
optimisticEntityMaps.object = objectResult.optimisticFlatEntityMaps;
allObjectAndFieldActions.push(
...objectResult.workspaceMigration.actions,
);
}
}
if (entityMaps.view) {
const viewResult =
await this.workspaceViewMigrationBuilderV2Service.validateAndBuild({
fromFlatViewMaps: entityMaps.view.fromFlatViewMaps,
toFlatViewMaps: entityMaps.view.toFlatViewMaps,
workspaceId,
buildOptions,
dependencyOptimisticEntityMaps: {
object: optimisticEntityMaps.object,
},
});
if (viewResult.status === 'fail') {
allFailures.push(...viewResult.errors);
} else {
optimisticEntityMaps.view = viewResult.optimisticFlatEntityMaps;
allViewActions.push(...viewResult.workspaceMigration.actions);
}
}
if (entityMaps.viewField) {
const viewFieldResult =
await this.workspaceViewFieldMigrationBuilderV2Service.validateAndBuild(
{
fromFlatViewFieldMaps: entityMaps.viewField.fromFlatViewFieldMaps,
toFlatViewFieldMaps: entityMaps.viewField.toFlatViewFieldMaps,
workspaceId,
buildOptions,
dependencyOptimisticEntityMaps: {
object: optimisticEntityMaps.object,
view: optimisticEntityMaps.view,
},
},
);
if (viewFieldResult.status === 'fail') {
allFailures.push(...viewFieldResult.errors);
} else {
optimisticEntityMaps.viewField =
viewFieldResult.optimisticFlatEntityMaps;
allViewFieldActions.push(
...viewFieldResult.workspaceMigration.actions,
);
}
}
if (allFailures.length > 0) {
return {
status: 'fail',
errors: allFailures,
};
}
// TODO: return workspace migrations
return;
} catch (error) {
this.logger.error(error);
throw new WorkspaceMigrationV2Exception(
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR,
error.message,
);
}
}
// TODO: move to dedicated service
private async prepareOptimisticEntityMaps({
entityMaps,
workspaceId,
}: {
entityMaps: WorkspaceMigrationOrchestratorEntityMaps;
workspaceId: string;
}): Promise<WorkspaceMigrationOrchestratorOptimisticEntityMaps> {
const optimisticObjectMaps =
entityMaps.object?.fromFlatObjectMetadataMaps ??
(
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{ workspaceId },
)
).flatObjectMetadataMaps;
if (!isDefined(optimisticObjectMaps)) {
throw new WorkspaceMigrationOrchestratorException(
'Object metadata maps not found',
);
}
const optimisticViewMaps =
entityMaps.view?.fromFlatViewMaps ??
(
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId,
})
).flatViewMaps;
if (!isDefined(optimisticViewMaps)) {
throw new WorkspaceMigrationOrchestratorException('View maps not found');
}
// For ViewFields, if not provided, start with empty maps since ViewFields are optional
const optimisticViewFieldMaps = entityMaps.viewField
?.fromFlatViewFieldMaps ?? {
byId: {},
idByUniversalIdentifier: {},
};
return {
object: optimisticObjectMaps,
view: optimisticViewMaps,
viewField: optimisticViewFieldMaps,
};
}
}
@@ -11,6 +11,7 @@ import {
WorkspaceMigrationV2ExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration.exception';
// TODO: replace build logic by orchestrator service
@Injectable()
export class WorkspaceMigrationValidateBuildAndRunService {
private readonly logger = new Logger(
@@ -1,6 +0,0 @@
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
export type FlatViewField = Pick<
ViewFieldEntity,
'fieldMetadataId' | 'position' | 'isVisible' | 'size'
>;
@@ -1,9 +0,0 @@
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type FlatViewField } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-view-field.type';
export type FlatView = Pick<
ViewEntity,
'name' | 'objectMetadataId' | 'type' | 'isCustom' | 'key' | 'icon'
> & {
viewFields: FlatViewField[];
};
@@ -0,0 +1,41 @@
import { type FromTo } from 'twenty-shared/types';
import { type FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type';
import { type FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type';
import { type FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
import { type WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
export type WorkspaceMigrationOrchestratorEntityMaps = {
object?: FromTo<FlatObjectMetadataMaps, 'FlatObjectMetadataMaps'>;
field?: FromTo<FlatObjectMetadataMaps, 'FlatObjectMetadataMaps'>; // Fields are part of object maps for now
view?: FromTo<FlatViewMaps, 'FlatViewMaps'>;
viewField?: FromTo<FlatViewFieldMaps, 'FlatViewFieldMaps'>;
};
type UnwrapFromTo<T> = T extends FromTo<infer U, infer _Tag> ? U : T;
export type WorkspaceMigrationOrchestratorOptimisticEntityMaps = {
[K in keyof WorkspaceMigrationOrchestratorEntityMaps]?: UnwrapFromTo<
WorkspaceMigrationOrchestratorEntityMaps[K]
>;
};
export type WorkspaceMigrationOrchestratorBuildArgs = {
workspaceId: string;
buildOptions: WorkspaceMigrationV2BuilderOptions;
entityMaps: WorkspaceMigrationOrchestratorEntityMaps;
};
export type WorkspaceMigrationOrchestratorFailedResult = {
status: 'fail';
errors: (
| FailedFlatObjectMetadataValidation
| FailedFlatFieldMetadataValidation
| FailedFlatViewValidation
| FailedFlatViewFieldValidation
)[];
};
@@ -1033,85 +1033,6 @@ exports[`Workspace migration builder object actions test suite It should build a
},
"type": "create_object",
},
{
"type": "create_view",
"view": {
"icon": "IconList",
"isCustom": false,
"key": "INDEX",
"name": "All {objectLabelPlural}",
"objectMetadataId": Any<String>,
"type": "TABLE",
"viewFields": [
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 0,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 1,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 2,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 3,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 4,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 5,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 6,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 7,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 8,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 9,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 10,
"size": 180,
},
],
},
},
]
`;
@@ -2419,181 +2340,6 @@ exports[`Workspace migration builder object actions test suite It should build a
},
"type": "create_object",
},
{
"type": "create_view",
"view": {
"icon": "IconList",
"isCustom": false,
"key": "INDEX",
"name": "All {objectLabelPlural}",
"objectMetadataId": Any<String>,
"type": "TABLE",
"viewFields": [
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 0,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 1,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 2,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 3,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 4,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 5,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 6,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 7,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 8,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 9,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 10,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 11,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 12,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 13,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 14,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 15,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 16,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 17,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 18,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 19,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 20,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 21,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 22,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 23,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 24,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 25,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 26,
"size": 180,
},
],
},
},
]
`;
@@ -3246,85 +2992,6 @@ exports[`Workspace migration builder object actions test suite It should build a
},
"type": "create_index",
},
{
"type": "create_view",
"view": {
"icon": "IconList",
"isCustom": false,
"key": "INDEX",
"name": "All {objectLabelPlural}",
"objectMetadataId": Any<String>,
"type": "TABLE",
"viewFields": [
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 0,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 1,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 2,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 3,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 4,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 5,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 6,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 7,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 8,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 9,
"size": 180,
},
{
"fieldMetadataId": Any<String>,
"isVisible": true,
"position": 10,
"size": 180,
},
],
},
},
]
`;
@@ -44,7 +44,7 @@ const DELETE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
context: {
input: {
buildOptions: {
inferDeletionFromMissingObjectFieldIndex: true,
inferDeletionFromMissingEntities: true,
isSystemBuild: true,
},
fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
@@ -81,7 +81,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
},
expectedActionsTypeCounter: {
createObject: 1,
createView: 1,
},
},
},
@@ -103,7 +102,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
},
expectedActionsTypeCounter: {
createObject: 1,
createView: 1,
},
},
},
@@ -114,7 +112,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [
expectedActionsTypeCounter: {
createIndex: 1,
createObject: 1,
createView: 1,
},
input: {
fromFlatObjectMetadataMaps:
@@ -54,7 +54,6 @@ const expectedActionsTypeCounterChecker = ({
deleteObject: 0,
updateField: 0,
updateObject: 0,
createView: 0,
};
const actualActionsTypeCounter = workspaceMigration.actions.reduce(
(acc, action) => {
@@ -125,7 +124,7 @@ describe.each(allWorkspaceBuilderTestCases)(
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
buildOptions = {
inferDeletionFromMissingObjectFieldIndex: true,
inferDeletionFromMissingEntities: true,
isSystemBuild: false,
},
} = typeof input === 'function' ? input() : input;
@@ -155,7 +155,7 @@ export class WorkspaceMigrationV2FieldActionsBuilderService {
validateAndBuildResult.created.push(createFieldAction);
}
for (const flatFieldMetadataToDelete of buildOptions.inferDeletionFromMissingObjectFieldIndex
for (const flatFieldMetadataToDelete of buildOptions.inferDeletionFromMissingEntities
? deletedFlatFieldMetadatas
: []) {
const validationErrors =
@@ -103,7 +103,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService {
validateAndBuildResult.created.push(createObjectAction);
}
for (const flatObjectMetadataToDelete of buildOptions.inferDeletionFromMissingObjectFieldIndex
for (const flatObjectMetadataToDelete of buildOptions.inferDeletionFromMissingEntities
? deletedFlatObjectMetadatas
: []) {
const validationErrors =
@@ -0,0 +1,178 @@
import { Injectable } from '@nestjs/common';
import { FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
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 { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { compareTwoFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/compare-two-flat-view-field.util';
import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { type CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { WorkspaceViewFieldMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service';
import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import {
UpdateViewFieldAction,
WorkspaceMigrationViewFieldActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
import {
getWorkspaceMigrationV2ViewFieldCreateAction,
getWorkspaceMigrationV2ViewFieldDeleteAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-field-action';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service';
export type CreatedDeletedUpdatedViewFieldInputMatrix = FromTo<
FlatViewFieldMaps,
'FlatViewFieldMaps'
> &
CustomDeletedCreatedUpdatedMatrix<'flatViewField', FlatViewField> & {
buildOptions: WorkspaceViewFieldMigrationV2BuilderOptions;
fromFlatViewFieldMaps: FlatViewFieldMaps;
dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps;
};
type ValidateAndBuildViewFieldResult<T extends WorkspaceMigrationActionV2> = {
failed: FailedFlatViewFieldValidation[];
created: T[];
deleted: T[];
updated: T[];
optimisticFlatViewFieldMaps: FlatViewFieldMaps;
};
@Injectable()
export class WorkspaceMigrationV2ViewFieldActionsBuilderService {
constructor(
private readonly flatViewFieldValidatorService: FlatViewFieldValidatorService,
) {}
public async validateAndBuildViewFieldActions({
createdFlatViewField,
deletedFlatViewField,
updatedFlatViewField,
buildOptions,
fromFlatViewFieldMaps,
dependencyOptimisticEntityMaps,
}: CreatedDeletedUpdatedViewFieldInputMatrix): Promise<
ValidateAndBuildViewFieldResult<WorkspaceMigrationViewFieldActionV2>
> {
const validateAndBuildResult: ValidateAndBuildViewFieldResult<WorkspaceMigrationViewFieldActionV2> =
{
failed: [],
created: [],
deleted: [],
updated: [],
optimisticFlatViewFieldMaps: structuredClone(fromFlatViewFieldMaps),
};
if (
!isDefined(dependencyOptimisticEntityMaps.object) ||
!isDefined(dependencyOptimisticEntityMaps.view)
) {
throw new Error('Dependency optimistic entity maps are not defined');
}
for (const flatViewFieldToCreate of createdFlatViewField) {
const validationErrors =
await this.flatViewFieldValidatorService.validateFlatViewFieldCreation({
_existingFlatViewFieldMaps:
validateAndBuildResult.optimisticFlatViewFieldMaps,
flatViewFieldToValidate: flatViewFieldToCreate,
optimisticFlatObjectMetadataMaps:
dependencyOptimisticEntityMaps.object,
optimisticFlatViewMaps: dependencyOptimisticEntityMaps.view,
});
if (validationErrors.viewFieldLevelErrors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
validateAndBuildResult.optimisticFlatViewFieldMaps =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFieldToCreate,
flatEntityMaps: validateAndBuildResult.optimisticFlatViewFieldMaps,
});
const createViewFieldAction =
getWorkspaceMigrationV2ViewFieldCreateAction({
flatViewField: flatViewFieldToCreate,
});
validateAndBuildResult.created.push(createViewFieldAction);
}
for (const flatViewFieldToDelete of buildOptions.inferDeletionFromMissingEntities
? deletedFlatViewField
: []) {
const validationErrors =
this.flatViewFieldValidatorService.validateFlatViewFieldDeletion({
existingFlatViewFieldMaps:
validateAndBuildResult.optimisticFlatViewFieldMaps,
viewFieldIdToDelete: flatViewFieldToDelete.id,
optimisticFlatViewMaps: dependencyOptimisticEntityMaps.view,
});
if (validationErrors.viewFieldLevelErrors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
validateAndBuildResult.optimisticFlatViewFieldMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: flatViewFieldToDelete.id,
flatEntityMaps: validateAndBuildResult.optimisticFlatViewFieldMaps,
});
const deleteViewFieldAction =
getWorkspaceMigrationV2ViewFieldDeleteAction(flatViewFieldToDelete);
validateAndBuildResult.deleted.push(deleteViewFieldAction);
}
for (const {
from: fromFlatViewField,
to: toFlatViewField,
} of updatedFlatViewField) {
const viewFieldUpdatedProperties = compareTwoFlatViewField({
fromFlatViewField,
toFlatViewField,
});
if (viewFieldUpdatedProperties.length === 0) {
continue;
}
const validationErrors =
this.flatViewFieldValidatorService.validateFlatViewFieldUpdate({
existingFlatViewFieldMaps:
validateAndBuildResult.optimisticFlatViewFieldMaps,
updatedFlatViewField: toFlatViewField,
optimisticFlatViewMaps: dependencyOptimisticEntityMaps.view,
});
if (validationErrors.viewFieldLevelErrors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
validateAndBuildResult.optimisticFlatViewFieldMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: toFlatViewField,
flatEntityMaps: validateAndBuildResult.optimisticFlatViewFieldMaps,
});
const updateViewFieldAction: UpdateViewFieldAction = {
type: 'update_view_field',
viewFieldId: toFlatViewField.id,
updates: viewFieldUpdatedProperties,
};
validateAndBuildResult.updated.push(updateViewFieldAction);
}
return validateAndBuildResult;
}
}
@@ -0,0 +1,165 @@
import { Injectable } from '@nestjs/common';
import { FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
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 { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import { compareTwoFlatView } from 'src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util';
import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { type CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { WorkspaceViewMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service';
import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import {
UpdateViewAction,
WorkspaceMigrationViewActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
import {
getWorkspaceMigrationV2ViewCreateAction,
getWorkspaceMigrationV2ViewDeleteAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-action';
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service';
export type CreatedDeletedUpdatedViewInputMatrix = FromTo<
FlatViewMaps,
'FlatViewMaps'
> &
CustomDeletedCreatedUpdatedMatrix<'flatView', FlatView> & {
buildOptions: WorkspaceViewMigrationV2BuilderOptions;
fromFlatViewMaps: FlatViewMaps;
dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps;
};
type ValidateAndBuildViewResult<T extends WorkspaceMigrationActionV2> = {
failed: FailedFlatViewValidation[];
created: T[];
deleted: T[];
updated: T[];
optimisticFlatViewMaps: FlatViewMaps;
};
@Injectable()
export class WorkspaceMigrationV2ViewActionsBuilderService {
constructor(
private readonly flatViewValidatorService: FlatViewValidatorService,
) {}
public async validateAndBuildViewActions({
createdFlatView,
deletedFlatView,
updatedFlatView,
buildOptions,
fromFlatViewMaps,
dependencyOptimisticEntityMaps,
}: CreatedDeletedUpdatedViewInputMatrix): Promise<
ValidateAndBuildViewResult<WorkspaceMigrationViewActionV2>
> {
const validateAndBuildResult: ValidateAndBuildViewResult<WorkspaceMigrationViewActionV2> =
{
failed: [],
created: [],
deleted: [],
updated: [],
optimisticFlatViewMaps: structuredClone(fromFlatViewMaps),
};
if (!isDefined(dependencyOptimisticEntityMaps.object)) {
throw new Error('Dependency optimistic entity maps are not defined');
}
for (const flatViewToCreate of createdFlatView) {
const validationErrors =
await this.flatViewValidatorService.validateFlatViewCreation({
_existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps,
flatViewToValidate: flatViewToCreate,
optimisticFlatObjectMetadataMaps:
dependencyOptimisticEntityMaps.object,
});
if (validationErrors.viewLevelErrors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
validateAndBuildResult.optimisticFlatViewMaps =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewToCreate,
flatEntityMaps: validateAndBuildResult.optimisticFlatViewMaps,
});
const createViewAction = getWorkspaceMigrationV2ViewCreateAction({
flatView: flatViewToCreate,
});
validateAndBuildResult.created.push(createViewAction);
}
for (const flatViewToDelete of buildOptions.inferDeletionFromMissingEntities
? deletedFlatView
: []) {
const validationErrors =
this.flatViewValidatorService.validateFlatViewDeletion({
existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps,
viewIdToDelete: flatViewToDelete.id,
});
if (validationErrors.viewLevelErrors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
validateAndBuildResult.optimisticFlatViewMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: flatViewToDelete.id,
flatEntityMaps: validateAndBuildResult.optimisticFlatViewMaps,
});
const deleteViewAction =
getWorkspaceMigrationV2ViewDeleteAction(flatViewToDelete);
validateAndBuildResult.deleted.push(deleteViewAction);
}
for (const { from: fromFlatView, to: toFlatView } of updatedFlatView) {
const viewUpdatedProperties = compareTwoFlatView({
fromFlatView,
toFlatView,
});
if (viewUpdatedProperties.length === 0) {
continue;
}
const validationErrors =
this.flatViewValidatorService.validateFlatViewUpdate({
existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps,
updatedFlatView: toFlatView,
});
if (validationErrors.viewLevelErrors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
validateAndBuildResult.optimisticFlatViewMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: toFlatView,
flatEntityMaps: validateAndBuildResult.optimisticFlatViewMaps,
});
const updateViewAction: UpdateViewAction = {
type: 'update_view',
viewId: toFlatView.id,
updates: viewUpdatedProperties,
};
validateAndBuildResult.updated.push(updateViewAction);
}
return validateAndBuildResult;
}
}
@@ -1,41 +0,0 @@
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
type CreateViewAction,
type WorkspaceMigrationViewActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
type BuildWorkspaceMigrationViewActionsArgs = {
createdFlatObjectMetadatas: FlatObjectMetadata[];
};
const getCreateViewAction = (
flatObjectMetadata: FlatObjectMetadata,
): CreateViewAction => {
return {
type: 'create_view',
view: {
objectMetadataId: flatObjectMetadata.id,
type: ViewType.TABLE,
name: `All {objectLabelPlural}`,
key: ViewKey.INDEX,
isCustom: false,
icon: 'IconList',
viewFields: flatObjectMetadata.flatFieldMetadatas
.filter((field) => field.name !== 'id' && field.name !== 'deletedAt')
.map((field, index) => ({
fieldMetadataId: field.id,
position: index,
isVisible: true,
size: 180,
})),
},
};
};
export const buildWorkspaceMigrationViewActions = ({
createdFlatObjectMetadatas,
}: BuildWorkspaceMigrationViewActionsArgs): WorkspaceMigrationViewActionV2[] => {
return createdFlatObjectMetadatas.map(getCreateViewAction);
};
@@ -0,0 +1,102 @@
import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { type WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
export type SuccessfulEntityMigrationBuildResult<TFlatEntityMaps> = {
status: 'success';
workspaceMigration: WorkspaceMigrationV2;
optimisticFlatEntityMaps: TFlatEntityMaps;
};
export type FailedEntityMigrationBuildResult<TFailedValidation> = {
status: 'fail';
errors: TFailedValidation[];
};
export abstract class WorkspaceEntityMigrationBuilderV2Service<
TFlatEntityMaps extends FlatEntityMaps<TFlatEntity>,
TFlatEntity extends FlatEntity,
TFailedValidation,
TActionType extends WorkspaceMigrationActionV2,
TBuildArgs extends { workspaceId: string } & FromTo<TFlatEntityMaps, string>,
> {
public async validateAndBuild(
args: TBuildArgs,
): Promise<
| SuccessfulEntityMigrationBuildResult<TFlatEntityMaps>
| FailedEntityMigrationBuildResult<TFailedValidation>
> {
const { from: fromEntityMaps, to: toEntityMaps } =
this.extractFromToEntityMaps(args);
const fromEntities = Object.values(fromEntityMaps.byId).filter(isDefined);
const toEntities = Object.values(toEntityMaps.byId).filter(isDefined);
const { created, deleted, updated } = deletedCreatedUpdatedMatrixDispatcher(
{
from: fromEntities,
to: toEntities,
},
);
const result = await this.validateAndBuildActions({
...args,
created,
updated,
deleted,
});
if (result.failed.length > 0) {
return {
status: 'fail',
errors: result.failed,
};
}
return {
status: 'success',
workspaceMigration: {
workspaceId: args.workspaceId,
actions: [...result.deleted, ...result.created, ...result.updated],
},
optimisticFlatEntityMaps: result.optimisticMaps,
};
}
// TODO: Refactor, this is too hacky
private extractFromToEntityMaps(args: TBuildArgs): {
from: TFlatEntityMaps;
to: TFlatEntityMaps;
} {
const fromKey = Object.keys(args).find((key) =>
key.startsWith('from'),
) as keyof TBuildArgs;
const toKey = Object.keys(args).find((key) =>
key.startsWith('to'),
) as keyof TBuildArgs;
return {
from: args[fromKey] as TFlatEntityMaps,
to: args[toKey] as TFlatEntityMaps,
};
}
protected abstract validateAndBuildActions(
args: TBuildArgs & {
created: TFlatEntity[];
updated: { from: TFlatEntity; to: TFlatEntity }[];
deleted: TFlatEntity[];
},
): Promise<{
failed: TFailedValidation[];
created: TActionType[];
deleted: TActionType[];
updated: TActionType[];
optimisticMaps: TFlatEntityMaps;
}>;
}
@@ -10,20 +10,20 @@ import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-mana
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service';
import { buildWorkspaceMigrationIndexActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service';
import { buildWorkspaceMigrationViewActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder';
import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-field-matrix.util';
import { computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util';
import { getWorkspaceMigrationV2CreateIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-index-actions';
export type WorkspaceMigrationV2BuilderOptions = {
inferDeletionFromMissingObjectFieldIndex: boolean;
inferDeletionFromMissingEntities: boolean;
isSystemBuild: boolean;
};
type SuccessfulWorkspaceMigrationBuildResult = {
status: 'success';
workspaceMigration: WorkspaceMigrationV2;
optimisticFlatEntityMaps: FlatObjectMetadataMaps;
};
export type FailedWorkspaceMigrationBuildResult = {
@@ -71,11 +71,13 @@ export class WorkspaceMigrationBuilderV2Service {
to: toFlatObjectMetadatas,
});
// Should be handled separately from objects
const objectMetadataDeletedCreatedUpdatedIndex =
computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix(
updatedFlatObjectMetadatas,
);
// Should be handled separately from objects
const objectMetadataDeletedCreatedUpdatedFields =
computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix(
updatedFlatObjectMetadatas,
@@ -116,12 +118,7 @@ export class WorkspaceMigrationBuilderV2Service {
const indexWorkspaceMigrationActions = buildWorkspaceMigrationIndexActions({
objectMetadataDeletedCreatedUpdatedIndex,
inferDeletionFromMissingObjectFieldIndex:
buildOptions.inferDeletionFromMissingObjectFieldIndex,
});
// TODO: Remove this from here when we have a proper builder that handles views diffing independently
const viewWorkspaceMigrationActions = buildWorkspaceMigrationViewActions({
createdFlatObjectMetadatas, // should be flatViews, not objects.
buildOptions.inferDeletionFromMissingEntities,
});
const allValidateAndBuildResultFailures = [
@@ -141,17 +138,18 @@ export class WorkspaceMigrationBuilderV2Service {
workspaceMigration: {
workspaceId,
actions: [
...fieldActionsValidateAndBuildResult.deleted,
...fieldActionsValidateAndBuildResult.deleted, // Should be handled separately from objects
...objectActionsValidateAndBuildResult.deleted,
...objectActionsValidateAndBuildResult.created,
...objectActionsValidateAndBuildResult.updated,
...fieldActionsValidateAndBuildResult.created,
...fieldActionsValidateAndBuildResult.updated,
...fieldActionsValidateAndBuildResult.created, // Should be handled separately from objects
...fieldActionsValidateAndBuildResult.updated, // Should be handled separately from objects
...createdObjectMetadataCreateIndexActions,
...indexWorkspaceMigrationActions,
...viewWorkspaceMigrationActions,
...indexWorkspaceMigrationActions, // Should be handled separately from objects
],
},
optimisticFlatEntityMaps:
fieldActionsValidateAndBuildResult.optimisticFlatObjectMetadataMaps,
};
}
}
@@ -0,0 +1,79 @@
import { Injectable } from '@nestjs/common';
import { type FromTo } from 'twenty-shared/types';
import { FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
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 {
FailedEntityMigrationBuildResult,
WorkspaceEntityMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { WorkspaceMigrationViewFieldActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
export type WorkspaceViewFieldMigrationV2BuilderOptions = {
isSystemBuild: boolean;
inferDeletionFromMissingEntities: boolean;
};
export type FailedWorkspaceViewFieldMigrationBuildResult =
FailedEntityMigrationBuildResult<FailedFlatViewFieldValidation>;
export type WorkspaceViewFieldMigrationBuildArgs = {
workspaceId: string;
buildOptions: WorkspaceViewFieldMigrationV2BuilderOptions;
dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps;
} & FromTo<FlatViewFieldMaps, 'FlatViewFieldMaps'>;
@Injectable()
export class WorkspaceViewFieldMigrationBuilderV2Service extends WorkspaceEntityMigrationBuilderV2Service<
FlatViewFieldMaps,
FlatViewField,
FailedFlatViewFieldValidation,
WorkspaceMigrationViewFieldActionV2,
WorkspaceViewFieldMigrationBuildArgs
> {
constructor(
private readonly workspaceMigrationV2ViewFieldActionsBuilderService: WorkspaceMigrationV2ViewFieldActionsBuilderService,
) {
super();
}
protected async validateAndBuildActions(
args: WorkspaceViewFieldMigrationBuildArgs & {
created: FlatViewField[];
updated: { from: FlatViewField; to: FlatViewField }[];
deleted: FlatViewField[];
},
): Promise<{
failed: FailedFlatViewFieldValidation[];
created: WorkspaceMigrationViewFieldActionV2[];
deleted: WorkspaceMigrationViewFieldActionV2[];
updated: WorkspaceMigrationViewFieldActionV2[];
optimisticMaps: FlatViewFieldMaps;
}> {
const viewFieldActionsValidateAndBuildResult =
await this.workspaceMigrationV2ViewFieldActionsBuilderService.validateAndBuildViewFieldActions(
{
fromFlatViewFieldMaps: args.fromFlatViewFieldMaps,
toFlatViewFieldMaps: args.toFlatViewFieldMaps,
createdFlatViewField: args.created,
updatedFlatViewField: args.updated,
deletedFlatViewField: args.deleted,
buildOptions: args.buildOptions,
dependencyOptimisticEntityMaps: args.dependencyOptimisticEntityMaps,
},
);
return {
failed: viewFieldActionsValidateAndBuildResult.failed,
created: viewFieldActionsValidateAndBuildResult.created,
deleted: viewFieldActionsValidateAndBuildResult.deleted,
updated: viewFieldActionsValidateAndBuildResult.updated,
optimisticMaps:
viewFieldActionsValidateAndBuildResult.optimisticFlatViewFieldMaps,
};
}
}
@@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { type FromTo } from 'twenty-shared/types';
import { FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
import {
FailedEntityMigrationBuildResult,
WorkspaceEntityMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { WorkspaceMigrationViewActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
export type WorkspaceViewMigrationV2BuilderOptions = {
isSystemBuild: boolean;
inferDeletionFromMissingEntities: boolean;
};
export type FailedWorkspaceViewMigrationBuildResult =
FailedEntityMigrationBuildResult<FailedFlatViewValidation>;
export type WorkspaceViewMigrationBuildArgs = {
workspaceId: string;
buildOptions: WorkspaceViewMigrationV2BuilderOptions;
dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps;
} & FromTo<FlatViewMaps, 'FlatViewMaps'>;
@Injectable()
export class WorkspaceViewMigrationBuilderV2Service extends WorkspaceEntityMigrationBuilderV2Service<
FlatViewMaps,
FlatView,
FailedFlatViewValidation,
WorkspaceMigrationViewActionV2,
WorkspaceViewMigrationBuildArgs
> {
constructor(
private readonly workspaceMigrationV2ViewActionsBuilderService: WorkspaceMigrationV2ViewActionsBuilderService,
) {
super();
}
protected async validateAndBuildActions(
args: WorkspaceViewMigrationBuildArgs & {
created: FlatView[];
updated: { from: FlatView; to: FlatView }[];
deleted: FlatView[];
},
): Promise<{
failed: FailedFlatViewValidation[];
created: WorkspaceMigrationViewActionV2[];
deleted: WorkspaceMigrationViewActionV2[];
updated: WorkspaceMigrationViewActionV2[];
optimisticMaps: FlatViewMaps;
}> {
const viewActionsValidateAndBuildResult =
await this.workspaceMigrationV2ViewActionsBuilderService.validateAndBuildViewActions(
{
fromFlatViewMaps: args.fromFlatViewMaps,
toFlatViewMaps: args.toFlatViewMaps,
createdFlatView: args.created,
updatedFlatView: args.updated,
deletedFlatView: args.deleted,
buildOptions: args.buildOptions,
dependencyOptimisticEntityMaps: args.dependencyOptimisticEntityMaps,
},
);
return {
failed: viewActionsValidateAndBuildResult.failed,
created: viewActionsValidateAndBuildResult.created,
deleted: viewActionsValidateAndBuildResult.deleted,
updated: viewActionsValidateAndBuildResult.updated,
optimisticMaps: viewActionsValidateAndBuildResult.optimisticFlatViewMaps,
};
}
}
@@ -0,0 +1,11 @@
import { type FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type';
import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type ValidateAndBuildViewResult<T extends WorkspaceMigrationActionV2> = {
failed: FailedFlatViewValidation[];
created: T[];
deleted: T[];
updated: T[];
optimisticFlatViewMaps: FlatViewMaps;
};
@@ -2,12 +2,14 @@ import { type WorkspaceMigrationFieldActionV2 } from 'src/engine/workspace-manag
import { type WorkspaceMigrationIndexActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { type WorkspaceMigrationObjectActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
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';
export type WorkspaceMigrationActionV2 =
| WorkspaceMigrationObjectActionV2
| WorkspaceMigrationFieldActionV2
| WorkspaceMigrationIndexActionV2
| WorkspaceMigrationViewActionV2;
| WorkspaceMigrationViewActionV2
| WorkspaceMigrationViewFieldActionV2;
export type WorkspaceMigrationActionTypeV2 = WorkspaceMigrationActionV2['type'];
@@ -1,11 +1,37 @@
import { type FlatView } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-view.type';
import { type FromTo } from 'twenty-shared/types';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type FlatViewPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export type FlatViewPropertyUpdate<P extends FlatViewPropertiesToCompare> = {
property: P;
} & FromTo<ViewEntity[P]>;
export type CreateViewAction = {
type: 'create_view';
view: FlatView;
};
export type WorkspaceMigrationViewActionV2 = CreateViewAction;
export type UpdateViewAction = {
type: 'update_view';
viewId: string;
updates: Array<
{
[P in FlatViewPropertiesToCompare]: FlatViewPropertyUpdate<P>;
}[FlatViewPropertiesToCompare]
>;
};
export type DeleteViewAction = {
type: 'delete_view';
viewId: string;
};
export type WorkspaceMigrationViewActionV2 =
| CreateViewAction
| UpdateViewAction
| DeleteViewAction;
export type WorkspaceMigrationViewActionTypeV2 =
WorkspaceMigrationViewActionV2['type'];
@@ -0,0 +1,39 @@
import { type FromTo } from 'twenty-shared/types';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { type FlatViewFieldPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export type FlatViewFieldPropertyUpdate<
P extends FlatViewFieldPropertiesToCompare,
> = {
property: P;
} & FromTo<ViewFieldEntity[P]>;
export type CreateViewFieldAction = {
type: 'create_view_field';
viewField: FlatViewField;
};
export type UpdateViewFieldAction = {
type: 'update_view_field';
viewFieldId: string;
updates: Array<
{
[P in FlatViewFieldPropertiesToCompare]: FlatViewFieldPropertyUpdate<P>;
}[FlatViewFieldPropertiesToCompare]
>;
};
export type DeleteViewFieldAction = {
type: 'delete_view_field';
viewFieldId: string;
};
export type WorkspaceMigrationViewFieldActionV2 =
| CreateViewFieldAction
| UpdateViewFieldAction
| DeleteViewFieldAction;
export type WorkspaceMigrationViewFieldActionTypeV2 =
WorkspaceMigrationViewFieldActionV2['type'];
@@ -0,0 +1,21 @@
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import {
type CreateViewAction,
type DeleteViewAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
export const getWorkspaceMigrationV2ViewCreateAction = ({
flatView,
}: {
flatView: FlatView;
}): CreateViewAction => ({
type: 'create_view',
view: flatView,
});
export const getWorkspaceMigrationV2ViewDeleteAction = (
flatView: FlatView,
): DeleteViewAction => ({
type: 'delete_view',
viewId: flatView.id,
});
@@ -0,0 +1,21 @@
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import {
type CreateViewFieldAction,
type DeleteViewFieldAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
export const getWorkspaceMigrationV2ViewFieldCreateAction = ({
flatViewField,
}: {
flatViewField: FlatViewField;
}): CreateViewFieldAction => ({
type: 'create_view_field',
viewField: flatViewField,
});
export const getWorkspaceMigrationV2ViewFieldDeleteAction = (
flatViewField: FlatViewField,
): DeleteViewFieldAction => ({
type: 'delete_view_field',
viewFieldId: flatViewField.id,
});
@@ -0,0 +1,151 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception';
import { FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util';
@Injectable()
export class FlatViewFieldValidatorService {
constructor() {}
public validateFlatViewFieldUpdate({
existingFlatViewFieldMaps,
updatedFlatViewField,
optimisticFlatViewMaps,
}: {
existingFlatViewFieldMaps: FlatViewFieldMaps;
updatedFlatViewField: FlatViewField;
optimisticFlatViewMaps: FlatViewMaps;
}): FailedFlatViewFieldValidation {
const errors = [];
const optimisticFlatView =
optimisticFlatViewMaps.byId[updatedFlatViewField.viewId];
if (!isDefined(optimisticFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
}
const existingFlatViewField =
existingFlatViewFieldMaps.byId[updatedFlatViewField.id];
if (!isDefined(existingFlatViewField)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field not found`,
userFriendlyMessage: t`View field not found`,
});
}
return {
type: 'update_view_field',
viewFieldLevelErrors: errors,
failedViewFieldValidationMinimalInformation: {
id: updatedFlatViewField.id,
viewId: updatedFlatViewField.viewId,
fieldMetadataId: updatedFlatViewField.fieldMetadataId,
},
};
}
public validateFlatViewFieldDeletion({
existingFlatViewFieldMaps,
viewFieldIdToDelete,
optimisticFlatViewMaps,
}: {
existingFlatViewFieldMaps: FlatViewFieldMaps;
viewFieldIdToDelete: string;
optimisticFlatViewMaps: FlatViewMaps;
}): FailedFlatViewFieldValidation {
const errors = [];
const optimisticFlatView = optimisticFlatViewMaps.byId[viewFieldIdToDelete];
if (!isDefined(optimisticFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
}
const existingFlatViewField =
existingFlatViewFieldMaps.byId[viewFieldIdToDelete];
if (!isDefined(existingFlatViewField)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field not found`,
userFriendlyMessage: t`View field not found`,
});
}
return {
type: 'delete_view_field',
viewFieldLevelErrors: errors,
failedViewFieldValidationMinimalInformation: {
id: viewFieldIdToDelete,
},
};
}
public async validateFlatViewFieldCreation({
_existingFlatViewFieldMaps,
flatViewFieldToValidate,
optimisticFlatObjectMetadataMaps,
optimisticFlatViewMaps,
}: {
_existingFlatViewFieldMaps: FlatViewFieldMaps;
flatViewFieldToValidate: FlatViewField;
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
optimisticFlatViewMaps: FlatViewMaps;
}): Promise<FailedFlatViewFieldValidation> {
const errors = [];
const relatedFlatFieldMetadata =
findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId({
fieldMetadataId: flatViewFieldToValidate.fieldMetadataId,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
});
if (!isDefined(relatedFlatFieldMetadata)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Field metadata not found`,
userFriendlyMessage: t`Field metadata not found`,
});
}
const optimisticFlatView =
optimisticFlatViewMaps.byId[flatViewFieldToValidate.viewId];
if (!isDefined(optimisticFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
}
return {
type: 'create_view_field',
viewFieldLevelErrors: errors,
failedViewFieldValidationMinimalInformation: {
id: flatViewFieldToValidate.id,
viewId: flatViewFieldToValidate.viewId,
fieldMetadataId: flatViewFieldToValidate.fieldMetadataId,
},
};
}
}
@@ -0,0 +1,104 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception';
import { FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
@Injectable()
export class FlatViewValidatorService {
constructor() {}
public validateFlatViewUpdate({
existingFlatViewMaps,
updatedFlatView,
}: {
existingFlatViewMaps: FlatViewMaps;
updatedFlatView: FlatView;
}): FailedFlatViewValidation {
const errors = [];
const existingFlatView = existingFlatViewMaps.byId[updatedFlatView.id];
if (!isDefined(existingFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
}
return {
type: 'update_view',
viewLevelErrors: errors,
failedViewValidationMinimalInformation: {
id: updatedFlatView.id,
},
};
}
public validateFlatViewDeletion({
existingFlatViewMaps,
viewIdToDelete,
}: {
existingFlatViewMaps: FlatViewMaps;
viewIdToDelete: string;
}): FailedFlatViewValidation {
const errors = [];
const existingFlatView = existingFlatViewMaps.byId[viewIdToDelete];
if (!isDefined(existingFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
}
return {
type: 'delete_view',
viewLevelErrors: errors,
failedViewValidationMinimalInformation: {
id: viewIdToDelete,
},
};
}
public async validateFlatViewCreation({
_existingFlatViewMaps,
flatViewToValidate,
optimisticFlatObjectMetadataMaps,
}: {
_existingFlatViewMaps: FlatViewMaps;
flatViewToValidate: FlatView;
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
}): Promise<FailedFlatViewValidation> {
const optimisticFlatObjectMetadata =
optimisticFlatObjectMetadataMaps.byId[
flatViewToValidate.objectMetadataId
];
const errors = [];
if (!isDefined(optimisticFlatObjectMetadata)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Object metadata not found`,
userFriendlyMessage: t`Object metadata not found`,
});
}
return {
type: 'create_view',
viewLevelErrors: errors,
failedViewValidationMinimalInformation: {
id: flatViewToValidate.id,
},
};
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service';
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service';
@Module({
providers: [FlatViewValidatorService, FlatViewFieldValidatorService],
exports: [FlatViewValidatorService, FlatViewFieldValidatorService],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
@@ -6,10 +6,15 @@ import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/f
import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-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 { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
import { WorkspaceViewFieldMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service';
import { WorkspaceViewMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service';
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module';
@Module({
imports: [FeatureFlagModule],
imports: [FeatureFlagModule, WorkspaceMigrationBuilderValidatorsModule],
providers: [
WorkspaceMigrationBuilderV2Service,
FlatFieldMetadataValidatorService,
@@ -17,7 +22,15 @@ import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager
WorkspaceMigrationV2ObjectActionsBuilderService,
FlatObjectMetadataValidatorService,
WorkspaceMigrationV2FieldActionsBuilderService,
WorkspaceMigrationV2ViewActionsBuilderService,
WorkspaceViewMigrationBuilderV2Service,
WorkspaceViewFieldMigrationBuilderV2Service,
WorkspaceMigrationV2ViewFieldActionsBuilderService,
],
exports: [
WorkspaceMigrationBuilderV2Service,
WorkspaceViewMigrationBuilderV2Service,
WorkspaceViewFieldMigrationBuilderV2Service,
],
exports: [WorkspaceMigrationBuilderV2Service],
})
export class WorkspaceMigrationBuilderV2Module {}
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { CreateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-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 CreateViewFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_view_field',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateViewFieldAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { viewField } = action;
const viewFieldRepository =
queryRunner.manager.getRepository<ViewFieldEntity>(ViewFieldEntity);
await viewFieldRepository.save({
...viewField,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateViewFieldAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { DeleteViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-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 DeleteViewFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_view_field',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteViewFieldAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { viewFieldId } = action;
const viewFieldRepository =
queryRunner.manager.getRepository<ViewFieldEntity>(ViewFieldEntity);
await viewFieldRepository.delete({
id: viewFieldId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteViewFieldAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-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 UpdateViewFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_view_field',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateViewFieldAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { viewFieldId } = action;
const viewFieldRepository =
queryRunner.manager.getRepository<ViewEntity>(ViewEntity);
await viewFieldRepository.update(
viewFieldId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateViewFieldAction>,
): Promise<void> {
return;
}
}
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { CreateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@@ -21,25 +20,13 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
const { action, queryRunner, workspaceId } = context;
const { view } = action;
const { viewFields } = view;
const viewRepository =
queryRunner.manager.getRepository<ViewEntity>(ViewEntity);
const viewFieldRepository =
queryRunner.manager.getRepository<ViewFieldEntity>(ViewFieldEntity);
const createdView = await viewRepository.save({
await viewRepository.save({
...view,
workspaceId,
});
await viewFieldRepository.save(
viewFields.map((viewField) => ({
...viewField,
viewId: createdView.id,
workspaceId,
})),
);
}
async executeForWorkspaceSchema(
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { DeleteViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-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 DeleteViewActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_view',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteViewAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { viewId } = action;
const viewRepository =
queryRunner.manager.getRepository<ViewEntity>(ViewEntity);
await viewRepository.delete({
id: viewId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteViewAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { UpdateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-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 UpdateViewActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_view',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateViewAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { viewId } = action;
const viewRepository =
queryRunner.manager.getRepository<ViewEntity>(ViewEntity);
await viewRepository.update(
viewId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateViewAction>,
): Promise<void> {
return;
}
}
@@ -9,7 +9,12 @@ import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/wo
import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service';
import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service';
import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service';
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 { 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';
@Module({
imports: [WorkspaceSchemaManagerModule],
@@ -26,6 +31,12 @@ import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/wor
DeleteIndexActionHandlerService,
CreateViewActionHandlerService,
UpdateViewActionHandlerService,
DeleteViewActionHandlerService,
CreateViewFieldActionHandlerService,
UpdateViewFieldActionHandlerService,
DeleteViewFieldActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -135,6 +135,11 @@ export const applyWorkspaceMigrationActionOnFlatObjectMetadataMaps = <
case 'create_index':
case 'delete_index':
case 'create_view':
case 'update_view':
case 'delete_view':
case 'create_view_field':
case 'update_view_field':
case 'delete_view_field':
return flatObjectMetadataMaps;
default: {
assertUnreachable(
@@ -1,8 +1,14 @@
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type UpdateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type';
import { type UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
export const fromWorkspaceMigrationUpdateActionToPartialEntity = <
T extends UpdateFieldAction | UpdateObjectAction,
T extends
| UpdateFieldAction
| UpdateObjectAction
| UpdateViewAction
| UpdateViewFieldAction,
>(
action: T,
) => {
@@ -1,5 +1,8 @@
import { Module } from '@nestjs/common';
import { ViewCacheModule } from 'src/engine/core-modules/view/cache/services/view-cache.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module';
import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module';
@@ -8,8 +11,16 @@ import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/w
imports: [
WorkspaceMigrationBuilderV2Module,
WorkspaceMigrationRunnerV2Module,
WorkspaceMetadataCacheModule,
ViewCacheModule,
],
providers: [
WorkspaceMigrationValidateBuildAndRunService,
WorkspaceMigrationBuildOrchestratorService,
],
exports: [
WorkspaceMigrationValidateBuildAndRunService,
WorkspaceMigrationBuildOrchestratorService,
],
providers: [WorkspaceMigrationValidateBuildAndRunService],
exports: [WorkspaceMigrationValidateBuildAndRunService],
})
export class WorkspaceMigrationV2Module {}
@@ -5,5 +5,5 @@ import { Column, Index } from 'typeorm';
})
export abstract class SyncableEntity {
@Column({ nullable: true, type: 'uuid' })
universalIdentifier?: string | null;
universalIdentifier: string | null;
}