Plugging + improving the new orchestrator (#14430)

# Introduction
Initially this PR was about introducing integration coverage for view
field v2 tests
But feature itself wasn't finished, so ended up in a TDD style
devlopment

## The new orchestrator
Orchestrator will organize each flat entity diffing inferring either
validation errors or actions sequentially.
A new builder has been introduced when extended requires to define flat
entity validation utils and action generation for `delete` `update`
`create` operations. It's highly typed using generics

## View field integration testing
Introduce coverage on failing basic tests cases for view field
operations, successfull create is also covered. We could still add more
coverage, will be done later in following PRs

## Remaining tasks for upcoming PRs:
- [ ] rename builder methods names to add generate action in their names
- [x] refactor view validation to handle field and view uniqueness index
combination
- [ ] implem strict update validator on view and view field
- [ ] dynamic cache invalidation post run
- [ ] Add coverage to successful and failing view field operations
- [ ] error formatting summary computation
- [ ] extract types out of the workspace entity builder

## Some vision
Currently flat field metadata maps is nested in the flat object metadata
maps, making its build nested too. ( not extending the new generic
builder )
We will refactor this part for both to be stored extra flat such as the
other existing flat entities.
It will require a small refactor to the generic builder that will not
only have impact on the currently built flat entity optimistic cache but
to an other one ( for example when building a create object action it
will also have an impact flat field maps )
This commit is contained in:
Paul Rastoin
2025-09-15 14:32:57 +02:00
committed by GitHub
parent 2aed7ba647
commit 579f704baa
86 changed files with 5465 additions and 3888 deletions
@@ -0,0 +1,11 @@
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
export const EMPTY_ALL_FLAT_ENTITY_MAPS = {
flatObjectMetadataMaps: {
byId: {},
idByNameSingular: {},
},
flatViewFieldMaps: EMPTY_FLAT_ENTITY_MAPS,
flatViewMaps: EMPTY_FLAT_ENTITY_MAPS,
} as const satisfies AllFlatEntityMaps;
@@ -0,0 +1,7 @@
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';
export const EMPTY_FLAT_ENTITY_MAPS = {
byId: {},
idByUniversalIdentifier: {},
} as const satisfies FlatEntityMaps<FlatEntity>;
@@ -0,0 +1,10 @@
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export type AllFlatEntitiesByMetadataEngineName = {
// flatFieldMetadata: FlatFieldMetadata;
objectMetadata: FlatObjectMetadata;
view: FlatView;
viewField: FlatViewField;
};
@@ -0,0 +1,13 @@
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 FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
export type AllFlatEntityMaps = {
flatObjectMetadataMaps: FlatObjectMetadataMaps;
flatViewMaps: FlatViewMaps;
flatViewFieldMaps: FlatViewFieldMaps;
};
// export type AllFlatEntityMaps = {
// [P in keyof AllFlatEntities as `${P}Maps`]: FlatEntityMaps<AllFlatEntities[P]>;
// };
@@ -8,15 +8,18 @@ import {
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';
export type FindFlatEntityByIdInFlatEntityMapsOrThrowArgs<
T extends FlatEntity,
> = {
flatEntityMaps: FlatEntityMaps<T>;
flatEntityId: string;
};
export const findFlatEntityByIdInFlatEntityMapsOrThrow = <
T extends FlatEntity,
>({
flatEntityMaps,
flatEntityId,
}: {
flatEntityMaps: FlatEntityMaps<T>;
flatEntityId: string;
}): T => {
}: FindFlatEntityByIdInFlatEntityMapsOrThrowArgs<T>): T => {
const flatEntity = flatEntityMaps.byId[flatEntityId];
if (!isDefined(flatEntity)) {
@@ -0,0 +1,15 @@
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import {
findFlatEntityByIdInFlatEntityMapsOrThrow,
type FindFlatEntityByIdInFlatEntityMapsOrThrowArgs,
} from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
export const findFlatEntityByIdInFlatEntityMaps = <T extends FlatEntity>(
args: FindFlatEntityByIdInFlatEntityMapsOrThrowArgs<T>,
): T | undefined => {
try {
return findFlatEntityByIdInFlatEntityMapsOrThrow(args);
} catch {
return undefined;
}
};
@@ -0,0 +1,25 @@
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
export const getSubFlatEntityMapsOrThrow = <T extends FlatEntity>({
flatEntityIds,
flatEntityMaps,
}: {
flatEntityMaps: FlatEntityMaps<T>;
flatEntityIds: string[];
}): FlatEntityMaps<T> => {
return flatEntityIds.reduce<FlatEntityMaps<T>>((acc, flatEntityId) => {
const flatEntity = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId,
flatEntityMaps,
});
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity,
flatEntityMaps: acc,
});
}, EMPTY_FLAT_ENTITY_MAPS);
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
export const getSubFlatEntityMaps = <T extends FlatEntity>({
flatEntityIds,
flatEntityMaps,
}: {
flatEntityMaps: FlatEntityMaps<T>;
flatEntityIds: string[];
}): FlatEntityMaps<T> => {
return flatEntityIds.reduce<FlatEntityMaps<T>>((acc, flatEntityId) => {
const flatEntity = findFlatEntityByIdInFlatEntityMaps({
flatEntityId,
flatEntityMaps,
});
if (!isDefined(flatEntity)) {
return acc;
}
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity,
flatEntityMaps: acc,
});
}, EMPTY_FLAT_ENTITY_MAPS);
};
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
@Module({
imports: [TypeOrmModule.forFeature([ViewEntity])],
imports: [TypeOrmModule.forFeature([ViewEntity, ViewFieldEntity])],
providers: [ViewCacheService],
exports: [ViewCacheService],
})
@@ -3,8 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { fromViewFieldEntityToFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util';
import { generateFlatViewMaps } from 'src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util';
type GetExistingOrRecomputeFlatViewMapsResult = {
@@ -18,6 +21,8 @@ export class ViewCacheService {
constructor(
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
) {}
async getExistingOrRecomputeFlatViewMaps({
@@ -30,6 +35,7 @@ export class ViewCacheService {
where: {
workspaceId,
},
withDeleted: true,
relations: ['viewFields'],
select: {
viewFields: {
@@ -44,4 +50,34 @@ export class ViewCacheService {
flatViewMaps: existingFlatViewMaps,
};
}
public async getExistingFlatViewFieldMapsFromCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<{ flatViewFieldMaps: FlatViewFieldMaps }> {
// TODO: get from cache later
const existingViewFields = await this.viewFieldRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
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 };
}
}
@@ -0,0 +1,8 @@
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const FLAT_VIEW_EDITABLE_PROPERTIES = [
'isVisible',
'size',
'position',
'aggregateOperation',
] as const satisfies (keyof FlatViewField)[];
@@ -1,8 +1,7 @@
import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/flat-view-editable-properties.constant';
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',
...FLAT_VIEW_EDITABLE_PROPERTIES,
'deletedAt',
] as const satisfies (keyof FlatViewField)[];
@@ -1,5 +0,0 @@
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'>
>;
@@ -1,9 +0,0 @@
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>;
};
@@ -1,5 +0,0 @@
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export type FailedFlatViewValidationMinimalInformation = Partial<
Pick<FlatView, 'id' | 'name' | 'objectMetadataId'>
>;
@@ -1,9 +0,0 @@
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>;
};
@@ -1,4 +0,0 @@
export type FlatViewFieldValidationError = {
message: string;
code: string;
};
@@ -1,8 +0,0 @@
import { type ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception';
export type FlatViewValidationError = {
code: ViewExceptionCode;
message: string;
userFriendlyMessage?: string;
value?: unknown;
};
@@ -10,7 +10,7 @@ import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant';
import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/flat-view-editable-properties.constant';
import { type FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
@@ -39,12 +39,12 @@ export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdateViewFieldInput.update,
FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE,
FLAT_VIEW_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatViewFieldToUpdate,
properties: FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE,
properties: FLAT_VIEW_EDITABLE_PROPERTIES,
update: updatedEditableFieldProperties,
});
};
@@ -1,61 +1,34 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service';
import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
import { DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util';
import { fromDeleteViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/core-modules/view/flat-view/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util';
import { fromDestroyViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/core-modules/view/flat-view/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util';
import { fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow } from 'src/engine/core-modules/view/flat-view/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.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';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ViewFieldV2Service {
constructor(
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
private readonly workspaceMigrationOrchestratorService: WorkspaceMigrationBuildOrchestratorService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly viewCacheService: ViewCacheService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
) {}
// TODO: move to cache service
private async getExistingFlatViewFieldMapsFromCache(
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({
createViewFieldInput,
workspaceId,
@@ -63,8 +36,20 @@ export class ViewFieldV2Service {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
const { flatViewMaps: existingFlatViewMaps } =
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId,
});
const flatViewFieldToCreate =
fromCreateViewFieldInputToFlatViewFieldToCreate({
@@ -78,14 +63,18 @@ export class ViewFieldV2Service {
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
fromToAllFlatEntityMaps: {
flatViewFieldMaps: {
from: existingFlatViewFieldMaps,
to: toFlatViewFieldMaps,
},
},
dependencyAllFlatEntityMaps: {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatViewMaps: existingFlatViewMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -95,13 +84,16 @@ export class ViewFieldV2Service {
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating view field',
);
}
const recomputedExistingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFieldToCreate.id,
@@ -116,8 +108,10 @@ export class ViewFieldV2Service {
workspaceId: string;
updateViewFieldInput: UpdateViewFieldInput;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
const optimisticallyUpdatedFlatView =
fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow({
@@ -125,18 +119,22 @@ export class ViewFieldV2Service {
updateViewFieldInput,
});
const fromFlatViewFieldMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [optimisticallyUpdatedFlatView.id],
flatEntityMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatView,
flatEntityMaps: existingFlatViewFieldMaps,
flatEntityMaps: fromFlatViewFieldMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
fromToAllFlatEntityMaps: {
flatViewFieldMaps: {
from: fromFlatViewFieldMaps,
to: toFlatViewFieldMaps,
},
},
buildOptions: {
@@ -148,13 +146,16 @@ export class ViewFieldV2Service {
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating view field',
);
}
const recomputedExistingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatView.id,
@@ -169,8 +170,10 @@ export class ViewFieldV2Service {
deleteViewFieldInput: DeleteViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
const optimisticallyUpdatedFlatViewWithDeletedAt =
fromDeleteViewFieldInputToFlatViewFieldOrThrow({
@@ -184,12 +187,12 @@ export class ViewFieldV2Service {
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
fromToAllFlatEntityMaps: {
flatViewFieldMaps: {
from: existingFlatViewFieldMaps,
to: toFlatViewFieldMaps,
},
},
buildOptions: {
@@ -201,13 +204,16 @@ export class ViewFieldV2Service {
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while updating view field',
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view field',
);
}
const recomputedExistingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatViewWithDeletedAt.id,
@@ -222,8 +228,14 @@ export class ViewFieldV2Service {
destroyViewFieldInput: DestroyViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
await this.viewCacheService.getExistingFlatViewFieldMapsFromCache({
workspaceId,
});
const { flatViewMaps: existingFlatViewMaps } =
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId,
});
const existingViewFieldToDelete =
fromDestroyViewFieldInputToFlatViewFieldOrThrow({
@@ -231,20 +243,27 @@ export class ViewFieldV2Service {
flatViewFieldMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
const fromFlatViewFieldMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [existingViewFieldToDelete.id],
flatEntityMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: fromFlatViewFieldMaps,
entityToDeleteId: existingViewFieldToDelete.id,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
fromToAllFlatEntityMaps: {
flatViewFieldMaps: {
from: fromFlatViewFieldMaps,
to: toFlatViewFieldMaps,
},
},
dependencyAllFlatEntityMaps: {
flatViewMaps: existingFlatViewMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
@@ -254,7 +273,8 @@ export class ViewFieldV2Service {
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view field',
);
}
@@ -7,6 +7,7 @@ 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 { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
@@ -20,20 +21,24 @@ import {
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';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ViewV2Service {
constructor(
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly workspaceMigrationOrchestratorService: WorkspaceMigrationBuildOrchestratorService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly viewCacheService: ViewCacheService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
) {}
async createOne(viewData: Partial<ViewEntity>): Promise<ViewEntity> {
if (!isDefined(viewData.workspaceId)) {
const { workspaceId } = viewData;
if (!isDefined(workspaceId)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
@@ -47,9 +52,16 @@ export class ViewV2Service {
);
}
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const { flatViewMaps: existingFlatViewMaps } =
await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({
workspaceId: viewData.workspaceId,
workspaceId,
});
const flatViewFromCreateInput = fromPartialFlatViewToFlatViewWithDefault({
@@ -63,24 +75,28 @@ export class ViewV2Service {
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
view: {
fromFlatViewMaps: existingFlatViewMaps,
toFlatViewMaps,
fromToAllFlatEntityMaps: {
flatViewMaps: {
from: existingFlatViewMaps,
to: toFlatViewMaps,
},
},
dependencyAllFlatEntityMaps: {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId: viewData.workspaceId,
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating view',
);
}
@@ -129,19 +145,22 @@ export class ViewV2Service {
universalIdentifier: existingViewToUpdate.universalIdentifier ?? '',
});
const toFlatViewMaps: FlatViewMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatViewFromUpdateInput,
flatEntityMaps: existingFlatViewMaps,
});
const fromFlatViewMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [flatViewFromUpdateInput.id],
flatEntityMaps: existingFlatViewMaps,
});
const toFlatViewMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatViewFromUpdateInput,
flatEntityMaps: fromFlatViewMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
view: {
fromFlatViewMaps: existingFlatViewMaps,
toFlatViewMaps,
fromToAllFlatEntityMaps: {
flatViewMaps: {
from: fromFlatViewMaps,
to: toFlatViewMaps,
},
},
buildOptions: {
@@ -153,7 +172,8 @@ export class ViewV2Service {
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating view',
);
}
@@ -185,19 +205,23 @@ export class ViewV2Service {
);
}
const fromFlatViewMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [existingViewToDelete.id],
flatEntityMaps: existingFlatViewMaps,
});
const toFlatViewMaps: FlatViewMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: existingFlatViewMaps,
flatEntityMaps: fromFlatViewMaps,
entityToDeleteId: existingViewToDelete.id,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
entityMaps: {
view: {
fromFlatViewMaps: existingFlatViewMaps,
toFlatViewMaps,
fromToAllFlatEntityMaps: {
flatViewMaps: {
from: fromFlatViewMaps,
to: toFlatViewMaps,
},
},
buildOptions: {
@@ -209,7 +233,8 @@ export class ViewV2Service {
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view',
);
}
@@ -28,8 +28,14 @@ import {
ViewException,
ViewExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
export const viewGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
return workspaceMigrationBuilderExceptionV2Formatter(error);
}
if (error instanceof ViewException) {
switch (error.code) {
case ViewExceptionCode.VIEW_NOT_FOUND:
@@ -7,6 +7,7 @@ import { ViewGroupException } from 'src/engine/core-modules/view/exceptions/view
import { ViewSortException } from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewException } from 'src/engine/core-modules/view/exceptions/view.exception';
import { viewGraphqlApiExceptionHandler } from 'src/engine/core-modules/view/utils/view-graphql-api-exception-handler.util';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
@Catch(
ViewException,
@@ -15,6 +16,7 @@ import { viewGraphqlApiExceptionHandler } from 'src/engine/core-modules/view/uti
ViewFilterGroupException,
ViewGroupException,
ViewSortException,
WorkspaceMigrationBuilderExceptionV2,
)
export class ViewGraphqlApiExceptionFilter implements ExceptionFilter {
catch(
@@ -24,7 +26,8 @@ export class ViewGraphqlApiExceptionFilter implements ExceptionFilter {
| ViewFilterException
| ViewFilterGroupException
| ViewGroupException
| ViewSortException,
| ViewSortException
| WorkspaceMigrationBuilderExceptionV2,
) {
return viewGraphqlApiExceptionHandler(exception);
}