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);
}
@@ -77,18 +77,17 @@ export class FieldMetadataServiceV2 {
existingFlatObjectMetadataMaps,
});
const flatObjectMetadataMapsWithImpactedObject =
getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [
...new Set(
flatFieldMetadatasToDelete.map(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId,
),
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [
...new Set(
flatFieldMetadatasToDelete.map(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId,
),
],
});
),
],
});
const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete.reduce(
(flatObjectMetadataMaps, flatFieldMetadataToDelete) =>
@@ -97,7 +96,7 @@ export class FieldMetadataServiceV2 {
flatObjectMetadataMaps,
objectMetadataId: flatFieldMetadataToDelete.objectMetadataId,
}),
flatObjectMetadataMapsWithImpactedObject,
fromFlatObjectMetadataMaps,
);
const validateAndBuildResult =
@@ -107,8 +106,12 @@ export class FieldMetadataServiceV2 {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject,
toFlatObjectMetadataMaps,
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
},
workspaceId,
},
);
@@ -177,8 +180,12 @@ export class FieldMetadataServiceV2 {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -266,8 +273,12 @@ export class FieldMetadataServiceV2 {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -7,7 +7,6 @@ import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { compareTwoFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
@@ -18,6 +17,7 @@ import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export type ValidateOneFieldMetadataArgs<
T extends FieldMetadataType = FieldMetadataType,
@@ -38,11 +38,13 @@ export class FlatFieldMetadataValidatorService {
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: updatedFlatFieldMetadata,
workspaceId,
}: ValidateOneFieldMetadataArgs<T>): Promise<FailedFlatFieldMetadataValidation> {
const validationResult: FailedFlatFieldMetadataValidation = {
}: ValidateOneFieldMetadataArgs<T>): Promise<
FailedFlatEntityValidation<FlatFieldMetadata>
> {
const validationResult: FailedFlatEntityValidation<FlatFieldMetadata> = {
type: 'update_field',
errors: [],
fieldMinimalInformation: {
flatEntityMinimalInformation: {
id: updatedFlatFieldMetadata.id,
name: updatedFlatFieldMetadata.name,
objectMetadataId: updatedFlatFieldMetadata.objectMetadataId,
@@ -162,11 +164,11 @@ export class FlatFieldMetadataValidatorService {
}: {
flatFieldMetadataToDelete: FlatFieldMetadata;
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
}): FailedFlatFieldMetadataValidation {
const validationResult: FailedFlatFieldMetadataValidation = {
}): FailedFlatEntityValidation<FlatFieldMetadata> {
const validationResult: FailedFlatEntityValidation<FlatFieldMetadata> = {
type: 'delete_field',
errors: [],
fieldMinimalInformation: {
flatEntityMinimalInformation: {
id: flatFieldMetadataToDelete.id,
name: flatFieldMetadataToDelete.name,
objectMetadataId: flatFieldMetadataToDelete.objectMetadataId,
@@ -238,10 +240,12 @@ export class FlatFieldMetadataValidatorService {
flatFieldMetadataToValidate,
otherFlatObjectMetadataMapsToValidate,
workspaceId,
}: ValidateOneFieldMetadataArgs<T>): Promise<FailedFlatFieldMetadataValidation> {
const validationResult: FailedFlatFieldMetadataValidation = {
}: ValidateOneFieldMetadataArgs<T>): Promise<
FailedFlatEntityValidation<FlatFieldMetadata>
> {
const validationResult: FailedFlatEntityValidation<FlatFieldMetadata> = {
errors: [],
fieldMinimalInformation: {
flatEntityMinimalInformation: {
id: flatFieldMetadataToValidate.id,
name: flatFieldMetadataToValidate.name,
objectMetadataId: flatFieldMetadataToValidate.objectMetadataId,
@@ -1,9 +0,0 @@
import { type FieldMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type';
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
import { type WorkspaceMigrationFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
export type FailedFlatFieldMetadataValidation = {
type: WorkspaceMigrationFieldActionTypeV2;
errors: FlatFieldMetadataValidationError[];
fieldMinimalInformation: Partial<FieldMetadataMinimalInformation>;
};
@@ -23,13 +23,17 @@ export const throwOnFieldInputTranspilationsError: ThrowOnFieldInputTranspilatio
// We should create a dedicated exceptions instead of hacking through the WorkspaceMigrationBuilderExceptionV2
throw new WorkspaceMigrationBuilderExceptionV2(
{
errors: [
{
errors: failedInputTranspilationErrors,
type: 'create_field',
fieldMinimalInformation: {},
},
],
report: {
view: [],
viewField: [],
objectMetadata: [
{
errors: failedInputTranspilationErrors,
type: 'create_field',
flatEntityMinimalInformation: {},
},
],
},
status: 'fail',
},
errorLabel,
@@ -4,24 +4,23 @@ import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
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 { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util';
import { FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
import { FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util';
import { computeMorphOrRelationTargetFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/compute-morph-or-relation-target-flat-object-metadata-maps.util';
import { getDefaultFailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-default-failed-flat-object-metadata-validation.type';
import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util';
import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util';
import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util';
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
@Injectable()
@@ -36,19 +35,20 @@ export class FlatObjectMetadataValidatorService {
}: {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
updatedFlatObjectMetadata: FlatObjectMetadata;
}): FailedFlatObjectMetadataValidation {
const validationResult = getDefaultFailedFlatObjectMetadataValidation({
}): FailedFlatEntityValidation<FlatObjectMetadata> {
const validationResult: FailedFlatEntityValidation<FlatObjectMetadata> = {
type: 'update_object',
objectMinimalInformation: {
errors: [],
flatEntityMinimalInformation: {
id: updatedFlatObjectMetadata.id,
},
});
};
const existingFlatObjectMetadata =
existingFlatObjectMetadataMaps.byId[updatedFlatObjectMetadata.id];
if (!isDefined(existingFlatObjectMetadata)) {
validationResult.objectLevelErrors.push({
validationResult.errors.push({
code: ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
message: t`Object to update not found`,
userFriendlyMessage: t`Object to update not found`,
@@ -56,20 +56,20 @@ export class FlatObjectMetadataValidatorService {
return validationResult;
}
validationResult.objectMinimalInformation = {
validationResult.flatEntityMinimalInformation = {
id: existingFlatObjectMetadata.id,
namePlural: existingFlatObjectMetadata.namePlural,
nameSingular: existingFlatObjectMetadata.nameSingular,
};
validationResult.objectLevelErrors.push(
validationResult.errors.push(
...this.validateFlatObjectMetadataNameAndLabels({
existingFlatObjectMetadataMaps,
flatObjectMetadataToValidate: updatedFlatObjectMetadata,
}),
);
validationResult.objectLevelErrors.push(
validationResult.errors.push(
...validateFlatObjectMetadataIdentifiers(existingFlatObjectMetadata),
);
@@ -84,33 +84,34 @@ export class FlatObjectMetadataValidatorService {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
objectMetadataToDeleteId: string;
buildOptions: WorkspaceMigrationV2BuilderOptions;
}): FailedFlatObjectMetadataValidation {
const validationResult = getDefaultFailedFlatObjectMetadataValidation({
}): FailedFlatEntityValidation<FlatObjectMetadata> {
const validationResult: FailedFlatEntityValidation<FlatObjectMetadata> = {
type: 'delete_object',
objectMinimalInformation: {
errors: [],
flatEntityMinimalInformation: {
id: objectMetadataToDeleteId,
},
});
};
const flatObjectMetadataToDelete =
existingFlatObjectMetadataMaps.byId[objectMetadataToDeleteId];
if (!isDefined(flatObjectMetadataToDelete)) {
validationResult.objectLevelErrors.push({
validationResult.errors.push({
code: ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
message: t`Object to delete not found`,
userFriendlyMessage: t`Object to delete not found`,
value: objectMetadataToDeleteId,
});
} else {
validationResult.objectMinimalInformation = {
validationResult.flatEntityMinimalInformation = {
id: flatObjectMetadataToDelete.id,
namePlural: flatObjectMetadataToDelete.namePlural,
nameSingular: flatObjectMetadataToDelete.nameSingular,
};
if (flatObjectMetadataToDelete.isRemote) {
validationResult.objectLevelErrors.push({
validationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Remote objects are not supported yet`,
userFriendlyMessage: t`Remote objects are not supported yet`,
@@ -121,7 +122,7 @@ export class FlatObjectMetadataValidatorService {
!buildOptions.isSystemBuild &&
isStandardMetadata(flatObjectMetadataToDelete)
) {
validationResult.objectLevelErrors.push({
validationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Standard objects cannot be deleted`,
userFriendlyMessage: t`Standard objects cannot be deleted`,
@@ -129,7 +130,7 @@ export class FlatObjectMetadataValidatorService {
}
if (!buildOptions.isSystemBuild && flatObjectMetadataToDelete.isActive) {
validationResult.objectLevelErrors.push({
validationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Active objects cannot be deleted`,
userFriendlyMessage: t`Active objects cannot be deleted`,
@@ -148,15 +149,21 @@ export class FlatObjectMetadataValidatorService {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
flatObjectMetadataToValidate: FlatObjectMetadata;
otherFlatObjectMetadataMapsToValidate?: FlatObjectMetadataMaps;
}): Promise<FailedFlatObjectMetadataValidation> {
const validationResult = getDefaultFailedFlatObjectMetadataValidation({
}): Promise<{
objectValidationResult: FailedFlatEntityValidation<FlatObjectMetadata>;
fieldValidationResults: FailedFlatEntityValidation<FlatFieldMetadata>[];
}> {
const objectValidationResult: FailedFlatEntityValidation<
FlatObjectMetadata | FlatFieldMetadata
> = {
type: 'create_object',
objectMinimalInformation: {
errors: [],
flatEntityMinimalInformation: {
id: flatObjectMetadataToValidate.id,
namePlural: flatObjectMetadataToValidate.namePlural,
nameSingular: flatObjectMetadataToValidate.nameSingular,
},
});
};
if (
isDefined(
@@ -166,7 +173,7 @@ export class FlatObjectMetadataValidatorService {
}),
)
) {
validationResult.objectLevelErrors.push({
objectValidationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Object with same id already exists`,
userFriendlyMessage: t`Object with same id already exists`,
@@ -174,21 +181,21 @@ export class FlatObjectMetadataValidatorService {
}
if (flatObjectMetadataToValidate.isRemote) {
validationResult.objectLevelErrors.push({
objectValidationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Remote objects are not supported yet`,
userFriendlyMessage: t`Remote objects are not supported yet`,
});
}
validationResult.objectLevelErrors.push(
objectValidationResult.errors.push(
...this.validateFlatObjectMetadataNameAndLabels({
existingFlatObjectMetadataMaps,
flatObjectMetadataToValidate,
}),
);
const allFlatFieldMetadatasValidationErrors: FailedFlatFieldMetadataValidation[] =
const fieldValidationResults: FailedFlatEntityValidation<FlatFieldMetadata>[] =
[];
let optimisticFlatObjectMetadataMaps =
addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({
@@ -220,7 +227,7 @@ export class FlatObjectMetadataValidatorService {
);
if (flatFieldValidatorErrors.errors.length > 0) {
allFlatFieldMetadatasValidationErrors.push(flatFieldValidatorErrors);
fieldValidationResults.push(flatFieldValidatorErrors);
continue;
}
@@ -231,13 +238,10 @@ export class FlatObjectMetadataValidatorService {
});
}
if (allFlatFieldMetadatasValidationErrors.length > 0) {
validationResult.fieldLevelErrors.push(
...allFlatFieldMetadatasValidationErrors,
);
}
return validationResult;
return {
fieldValidationResults,
objectValidationResult,
};
}
private validateFlatObjectMetadataNameAndLabels({
@@ -1,11 +0,0 @@
import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type';
import { type WorkspaceMigrationObjectActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
export type FailedFlatObjectMetadataValidation = {
type: WorkspaceMigrationObjectActionTypeV2;
objectLevelErrors: FlatObjectMetadataValidationError[];
fieldLevelErrors: FailedFlatFieldMetadataValidation[];
objectMinimalInformation: Partial<ObjectMetadataMinimalInformation>;
};
@@ -1,11 +0,0 @@
import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
export const getDefaultFailedFlatObjectMetadataValidation = (
overrides: Partial<Omit<FailedFlatObjectMetadataValidation, 'type'>> &
Pick<FailedFlatObjectMetadataValidation, 'type'>,
): FailedFlatObjectMetadataValidation => ({
fieldLevelErrors: [],
objectLevelErrors: [],
objectMinimalInformation: {},
...overrides,
});
@@ -1,15 +1,14 @@
import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import {
WORKSPACE_MIGRATION_OBJECT_ACTION_TYPES,
type WorkspaceMigrationObjectActionTypeV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
export const isFailedFlatObjectMetadataValidation = (
failedValidation:
| FailedFlatObjectMetadataValidation
| FailedFlatFieldMetadataValidation,
): failedValidation is FailedFlatObjectMetadataValidation => {
failedValidation: FailedFlatEntityValidation<FlatEntity>,
): failedValidation is FailedFlatEntityValidation<FlatObjectMetadata> => {
return WORKSPACE_MIGRATION_OBJECT_ACTION_TYPES.includes(
failedValidation.type as WorkspaceMigrationObjectActionTypeV2,
);
@@ -1,11 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
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';
@@ -27,7 +23,6 @@ import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
@@ -38,11 +33,7 @@ export class ObjectMetadataServiceV2 {
constructor(
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly viewV2Service: ViewV2Service,
) {}
async updateOne({
@@ -93,8 +84,12 @@ export class ObjectMetadataServiceV2 {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -196,8 +191,12 @@ export class ObjectMetadataServiceV2 {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
},
buildOptions: {
inferDeletionFromMissingEntities: true,
isSystemBuild: false,
@@ -285,8 +284,12 @@ export class ObjectMetadataServiceV2 {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -1,8 +1,8 @@
import { type FailedWorkspaceMigrationBuildResult } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
import { type WorkspaceMigrationOrchestratorFailedResult } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
export class WorkspaceMigrationBuilderExceptionV2 extends Error {
constructor(
public readonly failedWorkspaceMigrationBuildResult: FailedWorkspaceMigrationBuildResult,
public readonly failedWorkspaceMigrationBuildResult: WorkspaceMigrationOrchestratorFailedResult,
message = 'Workspace migration builder failed',
) {
super(message);
@@ -1,6 +0,0 @@
export class WorkspaceMigrationOrchestratorException extends Error {
constructor(message = 'Workspace migration orchestrator failed') {
super(message);
this.name = 'WorkspaceMigrationOrchestratorException';
}
}
@@ -1,7 +1,9 @@
import { type AllFlatEntitiesByMetadataEngineName } from 'src/engine/core-modules/common/types/all-flat-entities.type';
import { type FieldMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type';
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type';
import { type OrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { type WorkspaceMigrationFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type WorkspaceMigrationObjectActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
@@ -16,14 +18,12 @@ export type ValidationErrorObjectResponse =
errors: FlatObjectMetadataValidationError[];
fields: ValidationErrorFieldResponse[];
};
export type ValidationErrorResponse = {
summary: {
totalErrors: number;
invalidFields: number;
invalidObjects: number;
};
errors: {
objectMetadata: ValidationErrorObjectResponse[];
fieldMetadata: ValidationErrorFieldResponse[];
} & {
[P in keyof AllFlatEntitiesByMetadataEngineName as `invalid${Capitalize<P>}s`]: number;
};
errors: OrchestratorFailureReport;
};
@@ -1,18 +0,0 @@
import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type ValidationErrorFieldResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type';
export const fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse =
(
failedFlatFieldMetadataValidation: FailedFlatFieldMetadataValidation,
): ValidationErrorFieldResponse => {
const { id, name, objectMetadataId } =
failedFlatFieldMetadataValidation.fieldMinimalInformation;
return {
operation: failedFlatFieldMetadataValidation.type,
id,
name,
errors: failedFlatFieldMetadataValidation.errors,
objectMetadataId,
};
};
@@ -1,24 +0,0 @@
import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
import { type ValidationErrorObjectResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type';
import { fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util';
export const fromFailedFlatObjectMetadataValidationToValidationErrorObjectResponse =
(
failedFlatObjectMetadataValidation: FailedFlatObjectMetadataValidation,
): ValidationErrorObjectResponse => {
const { id, namePlural, nameSingular } =
failedFlatObjectMetadataValidation.objectMinimalInformation;
const fields = failedFlatObjectMetadataValidation.fieldLevelErrors.map(
fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse,
);
const objectResponseError: ValidationErrorObjectResponse = {
operation: failedFlatObjectMetadataValidation.type,
fields,
id,
namePlural,
nameSingular,
errors: failedFlatObjectMetadataValidation.objectLevelErrors,
};
return objectResponseError;
};
@@ -1,65 +1,27 @@
import { isFailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/utils/is-failed-flat-object-metadata-validation.util';
import { type WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { type ValidationErrorResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type';
import { fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-field-metadata-validation-to-validation-error-field-response.util';
import { fromFailedFlatObjectMetadataValidationToValidationErrorObjectResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-failed-flat-object-metadata-validation-to-validation-error-object-response.util';
export const fromWorkspaceMigrationBuilderExceptionToValidationResponseError = (
workspaceMigrationBuilderException: WorkspaceMigrationBuilderExceptionV2,
) => {
): ValidationErrorResponse => {
const emptyResponseError: ValidationErrorResponse = {
summary: {
invalidFields: 0,
invalidObjects: 0,
invalidObjectMetadatas: 0,
invalidViews: 0,
invalidViewFields: 0,
totalErrors: 0,
},
errors: {
fieldMetadata: [],
objectMetadata: [],
view: [],
viewField: [],
},
};
return workspaceMigrationBuilderException.failedWorkspaceMigrationBuildResult.errors.reduce<ValidationErrorResponse>(
({ errors, summary }, failedValidationError) => {
if (isFailedFlatObjectMetadataValidation(failedValidationError)) {
const errorObjectResponse =
fromFailedFlatObjectMetadataValidationToValidationErrorObjectResponse(
failedValidationError,
);
return {
summary: {
...summary,
invalidFields:
summary.invalidFields + errorObjectResponse.fields.length,
invalidObjects: ++summary.invalidObjects,
totalErrors:
++summary.totalErrors + errorObjectResponse.fields.length,
},
errors: {
...errors,
objectMetadata: [...errors.objectMetadata, errorObjectResponse],
},
};
}
const fieldResponseError =
fromFailedFlatFieldMetadataValidationToValidationErrorFieldResponse(
failedValidationError,
);
return {
summary: {
...summary,
invalidFields: ++summary.invalidFields,
totalErrors: ++summary.totalErrors,
},
errors: {
...errors,
fieldMetadata: [...errors.fieldMetadata, fieldResponseError],
},
};
},
emptyResponseError,
);
return {
...emptyResponseError,
errors:
workspaceMigrationBuilderException.failedWorkspaceMigrationBuildResult
.report,
};
};
@@ -13,8 +13,8 @@ export const workspaceMigrationBuilderExceptionV2Formatter = (
const { errors, summary } =
fromWorkspaceMigrationBuilderExceptionToValidationResponseError(error);
const invalidObjects = summary.invalidObjects;
const invalidFields = summary.invalidFields;
const invalidObjects = summary.invalidObjectMetadatas;
const invalidFields = summary.invalidViewFields;
throw new BaseGraphQLError(error.message, ErrorCode.BAD_USER_INPUT, {
code: 'METADATA_VALIDATION_ERROR',
@@ -1,24 +1,19 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { isDefined } from 'twenty-shared/utils';
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 { EMPTY_ALL_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-all-flat-entity-maps.constant';
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import {
OrchestratorFailureReport,
WorkspaceMigrationOrchestratorBuildArgs,
WorkspaceMigrationOrchestratorEntityMaps,
WorkspaceMigrationOrchestratorFailedResult,
WorkspaceMigrationOrchestratorOptimisticEntityMaps,
WorkspaceMigrationOrchestratorSuccessfulResult,
} 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 { 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 { 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';
import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
@Injectable()
export class WorkspaceMigrationBuildOrchestratorService {
@@ -28,162 +23,154 @@ export class WorkspaceMigrationBuildOrchestratorService {
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,
private readonly workspaceMigrationV2ViewActionsBuilderService: WorkspaceMigrationV2ViewActionsBuilderService,
private readonly workspaceMigrationV2ViewFieldActionsBuilderService: WorkspaceMigrationV2ViewFieldActionsBuilderService,
) {}
public async buildWorkspaceMigrations({
workspaceId,
buildOptions,
entityMaps,
}: WorkspaceMigrationOrchestratorBuildArgs): Promise<
WorkspaceMigrationOrchestratorFailedResult | undefined
> {
try {
const allObjectAndFieldActions = [];
const allViewActions = [];
const allViewFieldActions = [];
const allFailures = [];
// This does not handle dependency maps correctly
private setupOptimisticCache({
fromToAllFlatEntityMaps,
dependencyAllFlatEntityMaps,
}: Pick<
WorkspaceMigrationOrchestratorBuildArgs,
'fromToAllFlatEntityMaps' | 'dependencyAllFlatEntityMaps'
>): AllFlatEntityMaps {
const allFromToFlatEntityMapsKeys = Object.keys(
fromToAllFlatEntityMaps,
) as (keyof AllFlatEntityMaps)[];
const optimisticEntityMaps = await this.prepareOptimisticEntityMaps({
entityMaps,
workspaceId,
});
return allFromToFlatEntityMapsKeys.reduce<AllFlatEntityMaps>(
(allFlatEntityMaps, currFlatMaps) => {
const fromToOccurence = fromToAllFlatEntityMaps[currFlatMaps];
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 (!isDefined(fromToOccurence)) {
return allFlatEntityMaps;
}
}
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,
...allFlatEntityMaps,
[currFlatMaps]: fromToOccurence.from,
};
}
// TODO: return workspace migrations
return;
} catch (error) {
this.logger.error(error);
throw new WorkspaceMigrationV2Exception(
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR,
error.message,
);
}
},
{
...EMPTY_ALL_FLAT_ENTITY_MAPS,
...dependencyAllFlatEntityMaps,
},
);
}
// TODO: move to dedicated service
private async prepareOptimisticEntityMaps({
entityMaps,
public async buildWorkspaceMigration({
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: {},
buildOptions,
fromToAllFlatEntityMaps,
dependencyAllFlatEntityMaps,
}: WorkspaceMigrationOrchestratorBuildArgs): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
const allActions: WorkspaceMigrationActionV2[] = [];
const orchestratorFailureReport: OrchestratorFailureReport = {
objectMetadata: [],
view: [],
viewField: [],
};
const optimisticAllFlatEntityMaps = this.setupOptimisticCache({
fromToAllFlatEntityMaps,
dependencyAllFlatEntityMaps,
});
const { flatObjectMetadataMaps, flatViewFieldMaps, flatViewMaps } =
fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
const { from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps } =
flatObjectMetadataMaps;
const objectResult =
await this.workspaceMigrationBuilderV2Service.validateAndBuild({
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
workspaceId,
buildOptions,
});
optimisticAllFlatEntityMaps.flatObjectMetadataMaps =
objectResult.optimisticFlatObjectMetadataMaps;
if (objectResult.status === 'fail') {
orchestratorFailureReport.objectMetadata.push(...objectResult.errors);
} else {
allActions.push(...objectResult.workspaceMigration.actions);
}
}
if (isDefined(flatViewMaps)) {
const { from: fromFlatViewMaps, to: toFlatViewMaps } = flatViewMaps;
const viewResult =
await this.workspaceMigrationV2ViewActionsBuilderService.validateAndBuild(
{
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
},
from: fromFlatViewMaps,
to: toFlatViewMaps,
buildOptions,
},
);
optimisticAllFlatEntityMaps.flatViewMaps =
viewResult.optimisticFlatEntityMaps;
if (viewResult.status === 'fail') {
orchestratorFailureReport.view.push(...viewResult.errors);
} else {
allActions.push(...viewResult.actions);
}
}
if (isDefined(flatViewFieldMaps)) {
const { from: fromFlatViewFieldMaps, to: toFlatViewFieldMaps } =
flatViewFieldMaps;
const viewFieldResult =
await this.workspaceMigrationV2ViewFieldActionsBuilderService.validateAndBuild(
{
from: fromFlatViewFieldMaps,
to: toFlatViewFieldMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
},
},
);
optimisticAllFlatEntityMaps.flatViewFieldMaps =
viewFieldResult.optimisticFlatEntityMaps;
if (viewFieldResult.status === 'fail') {
orchestratorFailureReport.viewField.push(...viewFieldResult.errors);
} else {
allActions.push(...viewFieldResult.actions);
}
}
const allErrors = Object.values(orchestratorFailureReport);
if (allErrors.some((report) => report.length > 0)) {
return {
status: 'fail',
report: orchestratorFailureReport,
};
}
return {
object: optimisticObjectMaps,
view: optimisticViewMaps,
viewField: optimisticViewFieldMaps,
status: 'success',
workspaceMigration: {
actions: allActions,
workspaceId,
},
};
}
}
@@ -1,10 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service';
import {
FailedWorkspaceMigrationBuildResult,
WorkspaceMigrationBuildArgs,
WorkspaceMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
WorkspaceMigrationOrchestratorBuildArgs,
WorkspaceMigrationOrchestratorFailedResult,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service';
import {
WorkspaceMigrationV2Exception,
@@ -19,22 +19,23 @@ export class WorkspaceMigrationValidateBuildAndRunService {
);
constructor(
private readonly workspaceMigrationBuilderV2Service: WorkspaceMigrationBuilderV2Service,
private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service,
private readonly workspaceMigrationBuildOrchestratorService: WorkspaceMigrationBuildOrchestratorService,
) {}
public async validateBuildAndRunWorkspaceMigration(
builderArgs: WorkspaceMigrationBuildArgs,
): Promise<FailedWorkspaceMigrationBuildResult | undefined> {
const validateAndBuildResult = await this.workspaceMigrationBuilderV2Service
.validateAndBuild(builderArgs)
.catch((error) => {
this.logger.error(error);
throw new WorkspaceMigrationV2Exception(
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR,
error.message,
);
});
builderArgs: WorkspaceMigrationOrchestratorBuildArgs,
): Promise<WorkspaceMigrationOrchestratorFailedResult | undefined> {
const validateAndBuildResult =
await this.workspaceMigrationBuildOrchestratorService
.buildWorkspaceMigration(builderArgs)
.catch((error) => {
this.logger.error(error);
throw new WorkspaceMigrationV2Exception(
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR,
error.message,
);
});
if (validateAndBuildResult.status === 'fail') {
return validateAndBuildResult;
@@ -1,41 +1,32 @@
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 AllFlatEntitiesByMetadataEngineName } from 'src/engine/core-modules/common/types/all-flat-entities.type';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-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]
>;
};
import { type WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
export type WorkspaceMigrationOrchestratorBuildArgs = {
workspaceId: string;
buildOptions: WorkspaceMigrationV2BuilderOptions;
entityMaps: WorkspaceMigrationOrchestratorEntityMaps;
fromToAllFlatEntityMaps: {
[P in keyof AllFlatEntityMaps]?: FromTo<AllFlatEntityMaps[P]>;
};
dependencyAllFlatEntityMaps?: Partial<AllFlatEntityMaps>;
};
export type OrchestratorFailureReport = {
[P in keyof AllFlatEntitiesByMetadataEngineName]: FailedFlatEntityValidation<
AllFlatEntitiesByMetadataEngineName[P]
>[];
};
export type WorkspaceMigrationOrchestratorFailedResult = {
status: 'fail';
errors: (
| FailedFlatObjectMetadataValidation
| FailedFlatFieldMetadataValidation
| FailedFlatViewValidation
| FailedFlatViewFieldValidation
)[];
report: OrchestratorFailureReport;
};
export type WorkspaceMigrationOrchestratorSuccessfulResult = {
status: 'success';
workspaceMigration: WorkspaceMigrationV2;
};
@@ -73,10 +73,13 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService {
);
if (
validationErrors.fieldLevelErrors.length > 0 ||
validationErrors.objectLevelErrors.length > 0
validationErrors.fieldValidationResults.length > 0 ||
validationErrors.objectValidationResult.errors.length > 0
) {
validateAndBuildResult.failed.push(validationErrors);
validateAndBuildResult.failed.push(
validationErrors.objectValidationResult,
...validationErrors.fieldValidationResults,
);
continue;
}
@@ -116,10 +119,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService {
},
);
if (
validationErrors.fieldLevelErrors.length > 0 ||
validationErrors.objectLevelErrors.length > 0
) {
if (validationErrors.errors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
@@ -160,10 +160,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService {
},
);
if (
validationErrors.fieldLevelErrors.length > 0 ||
validationErrors.objectLevelErrors.length > 0
) {
if (validationErrors.errors.length > 0) {
validateAndBuildResult.failed.push(validationErrors);
continue;
}
@@ -0,0 +1,15 @@
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import { type WorkspaceMigrationActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type FlatEntityValidationError<TCode extends string = string> = {
code: TCode; // should be a better extends
message: string;
userFriendlyMessage?: string;
value?: unknown;
};
export type FailedFlatEntityValidation<T extends FlatEntity> = {
type: WorkspaceMigrationActionTypeV2;
errors: FlatEntityValidationError[];
flatEntityMinimalInformation: Partial<T>;
};
@@ -1,19 +1,14 @@
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 { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-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 {
FlatEntityUpdateValidationArgs,
FlatEntityValidationArgs,
FlatEntityValidationReturnType,
WorkspaceEntityMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import {
UpdateViewFieldAction,
WorkspaceMigrationViewFieldActionV2,
@@ -24,155 +19,138 @@ import {
} 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;
};
export type ViewFieldRelatedFlatEntityMaps = Pick<
AllFlatEntityMaps,
'flatObjectMetadataMaps' | 'flatViewMaps'
>;
@Injectable()
export class WorkspaceMigrationV2ViewFieldActionsBuilderService {
export class WorkspaceMigrationV2ViewFieldActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
FlatViewField,
WorkspaceMigrationViewFieldActionV2,
ViewFieldRelatedFlatEntityMaps
> {
constructor(
private readonly flatViewFieldValidatorService: FlatViewFieldValidatorService,
) {}
) {
super();
}
public async validateAndBuildViewFieldActions({
createdFlatViewField,
deletedFlatViewField,
updatedFlatViewField,
buildOptions,
fromFlatViewFieldMaps,
dependencyOptimisticEntityMaps,
}: CreatedDeletedUpdatedViewFieldInputMatrix): Promise<
ValidateAndBuildViewFieldResult<WorkspaceMigrationViewFieldActionV2>
protected async validateFlatEntityCreation({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatViewFieldToValidate,
optimisticFlatEntityMaps: optimisticFlatViewFieldMaps,
}: FlatEntityValidationArgs<
FlatViewField,
ViewFieldRelatedFlatEntityMaps
>): Promise<
FlatEntityValidationReturnType<
WorkspaceMigrationViewFieldActionV2,
FlatViewField
>
> {
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,
const validationResult =
await this.flatViewFieldValidatorService.validateFlatViewFieldCreation({
dependencyOptimisticFlatEntityMaps,
flatViewFieldToValidate,
optimisticFlatViewFieldMaps,
});
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,
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
validateAndBuildResult.updated.push(updateViewFieldAction);
}
return validateAndBuildResult;
return {
status: 'success',
action: getWorkspaceMigrationV2ViewFieldCreateAction(
flatViewFieldToValidate,
),
};
}
protected async validateFlatEntityDeletion({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatViewFieldToValidate,
optimisticFlatEntityMaps: optimisticFlatViewFieldMaps,
}: FlatEntityValidationArgs<
FlatViewField,
ViewFieldRelatedFlatEntityMaps
>): Promise<
FlatEntityValidationReturnType<
WorkspaceMigrationViewFieldActionV2,
FlatViewField
>
> {
const validationResult =
this.flatViewFieldValidatorService.validateFlatViewFieldDeletion({
dependencyOptimisticFlatEntityMaps,
flatViewFieldToValidate,
optimisticFlatViewFieldMaps,
});
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
return {
status: 'success',
action: getWorkspaceMigrationV2ViewFieldDeleteAction(
flatViewFieldToValidate,
),
};
}
protected async validateFlatEntityUpdate({
dependencyOptimisticFlatEntityMaps,
flatEntityUpdate: { from: fromFlatViewField, to: toFlatViewField },
optimisticFlatEntityMaps: optimisticFlatViewFieldMaps,
}: FlatEntityUpdateValidationArgs<
FlatViewField,
ViewFieldRelatedFlatEntityMaps
>): Promise<
| FlatEntityValidationReturnType<
WorkspaceMigrationViewFieldActionV2,
FlatViewField
>
| undefined
> {
const viewFieldUpdatedProperties = compareTwoFlatViewField({
fromFlatViewField,
toFlatViewField,
});
if (viewFieldUpdatedProperties.length === 0) {
return undefined;
}
const validationResult =
this.flatViewFieldValidatorService.validateFlatViewFieldUpdate({
dependencyOptimisticFlatEntityMaps,
flatViewFieldToValidate: toFlatViewField,
optimisticFlatViewFieldMaps,
});
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const updateViewFieldAction: UpdateViewFieldAction = {
type: 'update_view_field',
viewFieldId: toFlatViewField.id,
updates: viewFieldUpdatedProperties,
};
return {
status: 'success',
action: updateViewFieldAction,
};
}
}
@@ -1,19 +1,14 @@
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 { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-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 {
FlatEntityUpdateValidationArgs,
FlatEntityValidationArgs,
FlatEntityValidationReturnType,
WorkspaceEntityMigrationBuilderV2Service,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import {
UpdateViewAction,
WorkspaceMigrationViewActionV2,
@@ -24,142 +19,120 @@ import {
} 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;
};
export type ViewRelatedFlatEntityMaps = Pick<
AllFlatEntityMaps,
'flatObjectMetadataMaps'
// 'flatFieldMetadataMaps' TODO implement once with extract field from object diffing
>;
@Injectable()
export class WorkspaceMigrationV2ViewActionsBuilderService {
export class WorkspaceMigrationV2ViewActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
FlatView,
WorkspaceMigrationViewActionV2,
ViewRelatedFlatEntityMaps
> {
constructor(
private readonly flatViewValidatorService: FlatViewValidatorService,
) {}
) {
super();
}
public async validateAndBuildViewActions({
createdFlatView,
deletedFlatView,
updatedFlatView,
buildOptions,
fromFlatViewMaps,
dependencyOptimisticEntityMaps,
}: CreatedDeletedUpdatedViewInputMatrix): Promise<
ValidateAndBuildViewResult<WorkspaceMigrationViewActionV2>
protected async validateFlatEntityCreation({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatViewToValidate,
optimisticFlatEntityMaps: optimisticFlatViewMaps,
}: FlatEntityValidationArgs<FlatView, ViewRelatedFlatEntityMaps>): Promise<
FlatEntityValidationReturnType<WorkspaceMigrationViewActionV2, FlatView>
> {
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,
const validationResult =
await this.flatViewValidatorService.validateFlatViewCreation({
dependencyOptimisticFlatEntityMaps,
flatViewToValidate,
optimisticFlatViewMaps,
});
validateAndBuildResult.created.push(createViewAction);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
for (const flatViewToDelete of buildOptions.inferDeletionFromMissingEntities
? deletedFlatView
: []) {
const validationErrors =
this.flatViewValidatorService.validateFlatViewDeletion({
existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps,
viewIdToDelete: flatViewToDelete.id,
});
return {
status: 'success',
action: getWorkspaceMigrationV2ViewCreateAction(flatViewToValidate),
};
}
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,
protected async validateFlatEntityDeletion({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatViewToValidate,
optimisticFlatEntityMaps: optimisticFlatViewMaps,
}: FlatEntityValidationArgs<FlatView, ViewRelatedFlatEntityMaps>): Promise<
FlatEntityValidationReturnType<WorkspaceMigrationViewActionV2, FlatView>
> {
const validationResult =
this.flatViewValidatorService.validateFlatViewDeletion({
dependencyOptimisticFlatEntityMaps,
flatViewToValidate,
optimisticFlatViewMaps,
});
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,
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
validateAndBuildResult.updated.push(updateViewAction);
}
return validateAndBuildResult;
return {
status: 'success',
action: getWorkspaceMigrationV2ViewDeleteAction(flatViewToValidate),
};
}
protected async validateFlatEntityUpdate({
dependencyOptimisticFlatEntityMaps,
flatEntityUpdate: { from: fromFlatView, to: toFlatView },
optimisticFlatEntityMaps: optimisticFlatViewMaps,
}: FlatEntityUpdateValidationArgs<
FlatView,
ViewRelatedFlatEntityMaps
>): Promise<
| FlatEntityValidationReturnType<WorkspaceMigrationViewActionV2, FlatView>
| undefined
> {
const viewUpdatedProperties = compareTwoFlatView({
fromFlatView,
toFlatView,
});
if (viewUpdatedProperties.length === 0) {
return undefined;
}
const validationResult =
this.flatViewValidatorService.validateFlatViewUpdate({
dependencyOptimisticFlatEntityMaps,
flatViewToValidate: toFlatView,
optimisticFlatViewMaps,
});
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const updateViewAction: UpdateViewAction = {
type: 'update_view',
viewId: toFlatView.id,
updates: viewUpdatedProperties,
};
return {
status: 'success',
action: updateViewAction,
};
}
}
@@ -1,102 +1,229 @@
import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
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 { 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 {
type DeletedCreatedUpdatedMatrix,
deletedCreatedUpdatedMatrixDispatcher,
} from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { type WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
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> = {
export type SuccessfulEntityMigrationBuildResult<
TActions extends WorkspaceMigrationActionV2,
TFlatEntity extends FlatEntity,
> = {
status: 'success';
workspaceMigration: WorkspaceMigrationV2;
optimisticFlatEntityMaps: TFlatEntityMaps;
actions: TActions[];
optimisticFlatEntityMaps: FlatEntityMaps<TFlatEntity>;
};
export type FailedEntityMigrationBuildResult<TFailedValidation> = {
export type FailedEntityMigrationBuildResult<TFlatEntity extends FlatEntity> = {
status: 'fail';
errors: TFailedValidation[];
errors: FailedFlatEntityValidation<TFlatEntity>[];
optimisticFlatEntityMaps: FlatEntityMaps<TFlatEntity>;
};
export type ValidateAndBuildArgs<
T extends FlatEntity,
TRelatedFlatEntityMaps extends Partial<AllFlatEntityMaps>,
> = {
buildOptions: WorkspaceMigrationV2BuilderOptions;
dependencyOptimisticFlatEntityMaps: TRelatedFlatEntityMaps;
} & FromTo<FlatEntityMaps<T>>;
export type ValidateAndBuildReturnType<
TActions extends WorkspaceMigrationActionV2,
TFlatEntity extends FlatEntity,
> =
| SuccessfulEntityMigrationBuildResult<TActions, TFlatEntity>
| FailedEntityMigrationBuildResult<TFlatEntity>;
export type ValidateAndBuildActionsArgs<
T extends FlatEntity,
TRelatedFlatEntityMaps extends Partial<AllFlatEntityMaps>,
> = ValidateAndBuildArgs<T, TRelatedFlatEntityMaps> &
DeletedCreatedUpdatedMatrix<T>;
export type ValidateAndBuilActionsReturnType<
TFlatEntity extends FlatEntity,
TActions extends WorkspaceMigrationActionV2,
> = {
failed: FailedFlatEntityValidation<TFlatEntity>[];
created: TActions[];
deleted: TActions[];
updated: TActions[];
};
export type FlatEntityValidationArgs<
TFlatEntity extends FlatEntity,
TRelatedFlatEntityMaps extends Partial<AllFlatEntityMaps>,
> = {
flatEntityToValidate: TFlatEntity;
optimisticFlatEntityMaps: FlatEntityMaps<TFlatEntity>;
dependencyOptimisticFlatEntityMaps: TRelatedFlatEntityMaps;
};
export type FlatEntityUpdateValidationArgs<
TFlatEntity extends FlatEntity,
TRelatedFlatEntityMaps extends Partial<AllFlatEntityMaps>,
> = Omit<
FlatEntityValidationArgs<TFlatEntity, TRelatedFlatEntityMaps>,
'flatEntityToValidate'
> & {
flatEntityUpdate: FromTo<TFlatEntity>;
};
export type FlatEntityValidationReturnType<
TActions extends WorkspaceMigrationActionV2,
TFlatEntity extends FlatEntity,
> =
| {
status: 'success';
action: TActions;
}
| ({
status: 'fail';
} & FailedFlatEntityValidation<TFlatEntity>);
export abstract class WorkspaceEntityMigrationBuilderV2Service<
TFlatEntityMaps extends FlatEntityMaps<TFlatEntity>,
TFlatEntity extends FlatEntity,
TFailedValidation,
TActionType extends WorkspaceMigrationActionV2,
TBuildArgs extends { workspaceId: string } & FromTo<TFlatEntityMaps, string>,
TActions extends WorkspaceMigrationActionV2,
TRelatedFlatEntityMaps extends Partial<AllFlatEntityMaps>,
> {
public async validateAndBuild(
args: TBuildArgs,
): Promise<
| SuccessfulEntityMigrationBuildResult<TFlatEntityMaps>
| FailedEntityMigrationBuildResult<TFailedValidation>
public async validateAndBuild({
buildOptions,
dependencyOptimisticFlatEntityMaps,
from: fromFlatEntityMaps,
to: toFlatEntityMaps,
}: ValidateAndBuildArgs<TFlatEntity, TRelatedFlatEntityMaps>): Promise<
ValidateAndBuildReturnType<TActions, TFlatEntity>
> {
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 fromFlatEntities = Object.values(fromFlatEntityMaps.byId).filter(
isDefined,
);
const toFlatEntities = Object.values(toFlatEntityMaps.byId).filter(
isDefined,
);
const { created, deleted, updated } = deletedCreatedUpdatedMatrixDispatcher(
{
from: fromEntities,
to: toEntities,
from: fromFlatEntities,
to: toFlatEntities,
},
);
let optimisticFlatEntityMaps = structuredClone(fromFlatEntityMaps);
const result = await this.validateAndBuildActions({
...args,
created,
updated,
deleted,
});
const validateAndBuildResult: ValidateAndBuilActionsReturnType<
TFlatEntity,
TActions
> = {
failed: [],
created: [],
deleted: [],
updated: [],
};
if (result.failed.length > 0) {
for (const flatEntityToCreate of created) {
const validationResult = await this.validateFlatEntityCreation({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatEntityToCreate,
optimisticFlatEntityMaps,
});
if (validationResult.status === 'fail') {
validateAndBuildResult.failed.push(validationResult);
continue;
}
optimisticFlatEntityMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatEntityToCreate,
flatEntityMaps: optimisticFlatEntityMaps,
});
validateAndBuildResult.created.push(validationResult.action);
}
for (const flatEntityToDelete of buildOptions.inferDeletionFromMissingEntities
? deleted
: []) {
const validationResult = await this.validateFlatEntityDeletion({
dependencyOptimisticFlatEntityMaps,
flatEntityToValidate: flatEntityToDelete,
optimisticFlatEntityMaps: optimisticFlatEntityMaps,
});
if (validationResult.status === 'fail') {
validateAndBuildResult.failed.push(validationResult);
continue;
}
optimisticFlatEntityMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: flatEntityToDelete.id,
flatEntityMaps: optimisticFlatEntityMaps,
});
validateAndBuildResult.deleted.push(validationResult.action);
}
for (const flatEntityUpdate of updated) {
const validationResult = await this.validateFlatEntityUpdate({
dependencyOptimisticFlatEntityMaps,
flatEntityUpdate,
optimisticFlatEntityMaps: optimisticFlatEntityMaps,
});
if (validationResult === undefined) {
continue;
}
if (validationResult.status === 'fail') {
validateAndBuildResult.failed.push(validationResult);
continue;
}
optimisticFlatEntityMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatEntityUpdate.to,
flatEntityMaps: optimisticFlatEntityMaps,
});
validateAndBuildResult.updated.push(validationResult.action);
}
if (validateAndBuildResult.failed.length > 0) {
return {
status: 'fail',
errors: result.failed,
errors: validateAndBuildResult.failed,
optimisticFlatEntityMaps,
};
}
return {
status: 'success',
workspaceMigration: {
workspaceId: args.workspaceId,
actions: [...result.deleted, ...result.created, ...result.updated],
},
optimisticFlatEntityMaps: result.optimisticMaps,
actions: [
...validateAndBuildResult.deleted,
...validateAndBuildResult.created,
...validateAndBuildResult.updated,
],
optimisticFlatEntityMaps,
};
}
// 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;
protected abstract validateFlatEntityCreation(
args: FlatEntityValidationArgs<TFlatEntity, TRelatedFlatEntityMaps>,
): Promise<FlatEntityValidationReturnType<TActions, TFlatEntity>>;
return {
from: args[fromKey] as TFlatEntityMaps,
to: args[toKey] as TFlatEntityMaps,
};
}
protected abstract validateFlatEntityDeletion(
args: FlatEntityValidationArgs<TFlatEntity, TRelatedFlatEntityMaps>,
): Promise<FlatEntityValidationReturnType<TActions, TFlatEntity>>;
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;
}>;
protected abstract validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<TFlatEntity, TRelatedFlatEntityMaps>,
): Promise<FlatEntityValidationReturnType<TActions, TFlatEntity> | undefined>;
}
@@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common';
import { type FromTo } from 'twenty-shared/types';
import { FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
import { fromFlatObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-maps-to-flat-object-metadatas.util';
import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
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 { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
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';
@@ -20,24 +20,23 @@ export type WorkspaceMigrationV2BuilderOptions = {
isSystemBuild: boolean;
};
type SuccessfulWorkspaceMigrationBuildResult = {
export type SuccessfulWorkspaceMigrationBuildResult = {
status: 'success';
workspaceMigration: WorkspaceMigrationV2;
optimisticFlatEntityMaps: FlatObjectMetadataMaps;
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
};
export type FailedWorkspaceMigrationBuildResult = {
status: 'fail';
errors: (
| FailedFlatObjectMetadataValidation
| FailedFlatFieldMetadataValidation
)[];
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
errors: FailedFlatEntityValidation<FlatEntity>[];
};
export type WorkspaceMigrationBuildArgs = {
workspaceId: string;
buildOptions: WorkspaceMigrationV2BuilderOptions;
} & FromTo<FlatObjectMetadataMaps, 'FlatObjectMetadataMaps'>;
// TODO deprecate this file
@Injectable()
export class WorkspaceMigrationBuilderV2Service {
constructor(
@@ -130,6 +129,8 @@ export class WorkspaceMigrationBuilderV2Service {
return {
status: 'fail',
errors: allValidateAndBuildResultFailures,
optimisticFlatObjectMetadataMaps:
fieldActionsValidateAndBuildResult.optimisticFlatObjectMetadataMaps,
};
}
@@ -148,7 +149,7 @@ export class WorkspaceMigrationBuilderV2Service {
...indexWorkspaceMigrationActions, // Should be handled separately from objects
],
},
optimisticFlatEntityMaps:
optimisticFlatObjectMetadataMaps:
fieldActionsValidateAndBuildResult.optimisticFlatObjectMetadataMaps,
};
}
@@ -1,79 +0,0 @@
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,
};
}
}
@@ -1,78 +0,0 @@
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,
};
}
}
@@ -1,15 +1,12 @@
import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.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 FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
export type ValidateAndBuildMetadataResult<
T extends WorkspaceMigrationActionV2,
> = {
failed: (
| FailedFlatFieldMetadataValidation
| FailedFlatObjectMetadataValidation
)[];
failed: FailedFlatEntityValidation<FlatEntity>[];
created: T[];
deleted: T[];
updated: T[];
@@ -1,11 +0,0 @@
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;
};
@@ -4,11 +4,9 @@ import {
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 => ({
export const getWorkspaceMigrationV2ViewCreateAction = (
flatView: FlatView,
): CreateViewAction => ({
type: 'create_view',
view: flatView,
});
@@ -4,11 +4,9 @@ import {
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 => ({
export const getWorkspaceMigrationV2ViewFieldCreateAction = (
flatViewField: FlatViewField,
): CreateViewFieldAction => ({
type: 'create_view_field',
viewField: flatViewField,
});
@@ -4,54 +4,43 @@ 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';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { ViewFieldRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
type ViewFieldValidationArgs = {
flatViewFieldToValidate: FlatViewField;
optimisticFlatViewFieldMaps: FlatViewFieldMaps;
dependencyOptimisticFlatEntityMaps: ViewFieldRelatedFlatEntityMaps;
};
@Injectable()
export class FlatViewFieldValidatorService {
constructor() {}
// Should implement strict checking on field compared field and their integrity passing the update array would be the best
public validateFlatViewFieldUpdate({
existingFlatViewFieldMaps,
updatedFlatViewField,
optimisticFlatViewMaps,
}: {
existingFlatViewFieldMaps: FlatViewFieldMaps;
updatedFlatViewField: FlatViewField;
optimisticFlatViewMaps: FlatViewMaps;
}): FailedFlatViewFieldValidation {
flatViewFieldToValidate: updatedFlatViewField,
optimisticFlatViewFieldMaps,
}: ViewFieldValidationArgs): FailedFlatEntityValidation<FlatViewField> {
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];
optimisticFlatViewFieldMaps.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`,
message: t`View field to update not found`,
userFriendlyMessage: t`View field to update not found`,
});
}
return {
type: 'update_view_field',
viewFieldLevelErrors: errors,
failedViewFieldValidationMinimalInformation: {
errors,
flatEntityMinimalInformation: {
id: updatedFlatViewField.id,
viewId: updatedFlatViewField.viewId,
fieldMetadataId: updatedFlatViewField.fieldMetadataId,
@@ -60,59 +49,65 @@ export class FlatViewFieldValidatorService {
}
public validateFlatViewFieldDeletion({
existingFlatViewFieldMaps,
viewFieldIdToDelete,
optimisticFlatViewMaps,
}: {
existingFlatViewFieldMaps: FlatViewFieldMaps;
viewFieldIdToDelete: string;
optimisticFlatViewMaps: FlatViewMaps;
}): FailedFlatViewFieldValidation {
optimisticFlatViewFieldMaps,
flatViewFieldToValidate: { id: viewFieldIdToDelete },
}: ViewFieldValidationArgs): FailedFlatEntityValidation<FlatViewField> {
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];
optimisticFlatViewFieldMaps.byId[viewFieldIdToDelete];
if (!isDefined(existingFlatViewField)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field not found`,
userFriendlyMessage: t`View field not found`,
message: t`View field to delete not found`,
userFriendlyMessage: t`View field to delete not found`,
});
} else {
if (!isDefined(existingFlatViewField.deletedAt)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field to delete has not been soft deleted`,
userFriendlyMessage: t`View field to delete has not been soft deleted`,
});
}
}
return {
type: 'delete_view_field',
viewFieldLevelErrors: errors,
failedViewFieldValidationMinimalInformation: {
errors,
flatEntityMinimalInformation: {
id: viewFieldIdToDelete,
},
};
}
public async validateFlatViewFieldCreation({
_existingFlatViewFieldMaps,
flatViewFieldToValidate,
optimisticFlatObjectMetadataMaps,
optimisticFlatViewMaps,
optimisticFlatViewFieldMaps,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatViewMaps: optimisticFlatViewMaps,
},
}: {
_existingFlatViewFieldMaps: FlatViewFieldMaps;
flatViewFieldToValidate: FlatViewField;
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
optimisticFlatViewMaps: FlatViewMaps;
}): Promise<FailedFlatViewFieldValidation> {
optimisticFlatViewFieldMaps: FlatViewFieldMaps;
dependencyOptimisticFlatEntityMaps: ViewFieldRelatedFlatEntityMaps;
}): Promise<FailedFlatEntityValidation<FlatViewField>> {
const errors = [];
if (
isDefined(optimisticFlatViewFieldMaps.byId[flatViewFieldToValidate.id])
) {
const flatViewFieldId = flatViewFieldToValidate.id;
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field metadata with id ${flatViewFieldId} already exists`,
userFriendlyMessage: t`View field metadata already exists`,
});
}
const relatedFlatFieldMetadata =
findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId({
fieldMetadataId: flatViewFieldToValidate.fieldMetadataId,
@@ -138,10 +133,28 @@ export class FlatViewFieldValidatorService {
});
}
const allFlatViewFields = Object.values(
optimisticFlatViewFieldMaps.byId,
).filter(isDefined);
const matchingFlatView = allFlatViewFields.find(
(flatViewField) =>
flatViewField.viewId === flatViewFieldToValidate.viewId &&
flatViewField.fieldMetadataId ===
flatViewFieldToValidate.fieldMetadataId,
);
if (isDefined(matchingFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field with same fieldmetadataId and viewId already exists`,
userFriendlyMessage: t`View field already exists`,
});
}
return {
type: 'create_view_field',
viewFieldLevelErrors: errors,
failedViewFieldValidationMinimalInformation: {
errors,
flatEntityMinimalInformation: {
id: flatViewFieldToValidate.id,
viewId: flatViewFieldToValidate.viewId,
fieldMetadataId: flatViewFieldToValidate.fieldMetadataId,
@@ -4,25 +4,27 @@ 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';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { ViewRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
type ViewValidationArgs = {
flatViewToValidate: FlatView;
optimisticFlatViewMaps: FlatViewMaps;
dependencyOptimisticFlatEntityMaps: ViewRelatedFlatEntityMaps;
};
@Injectable()
export class FlatViewValidatorService {
constructor() {}
public validateFlatViewUpdate({
existingFlatViewMaps,
updatedFlatView,
}: {
existingFlatViewMaps: FlatViewMaps;
updatedFlatView: FlatView;
}): FailedFlatViewValidation {
flatViewToValidate: updatedFlatView,
optimisticFlatViewMaps,
}: ViewValidationArgs): FailedFlatEntityValidation<FlatView> {
const errors = [];
const existingFlatView = existingFlatViewMaps.byId[updatedFlatView.id];
const existingFlatView = optimisticFlatViewMaps.byId[updatedFlatView.id];
if (!isDefined(existingFlatView)) {
errors.push({
@@ -34,23 +36,20 @@ export class FlatViewValidatorService {
return {
type: 'update_view',
viewLevelErrors: errors,
failedViewValidationMinimalInformation: {
errors,
flatEntityMinimalInformation: {
id: updatedFlatView.id,
},
};
}
public validateFlatViewDeletion({
existingFlatViewMaps,
viewIdToDelete,
}: {
existingFlatViewMaps: FlatViewMaps;
viewIdToDelete: string;
}): FailedFlatViewValidation {
flatViewToValidate: { id: viewIdToDelete },
optimisticFlatViewMaps,
}: ViewValidationArgs): FailedFlatEntityValidation<FlatView> {
const errors = [];
const existingFlatView = existingFlatViewMaps.byId[viewIdToDelete];
const existingFlatView = optimisticFlatViewMaps.byId[viewIdToDelete];
if (!isDefined(existingFlatView)) {
errors.push({
@@ -58,30 +57,32 @@ export class FlatViewValidatorService {
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
} else {
if (!isDefined(existingFlatView.deletedAt)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View to delete has not been soft deleted`,
userFriendlyMessage: t`View to delete has not been soft deleted`,
});
}
}
return {
type: 'delete_view',
viewLevelErrors: errors,
failedViewValidationMinimalInformation: {
errors,
flatEntityMinimalInformation: {
id: viewIdToDelete,
},
};
}
public async validateFlatViewCreation({
_existingFlatViewMaps,
dependencyOptimisticFlatEntityMaps: { flatObjectMetadataMaps },
flatViewToValidate,
optimisticFlatObjectMetadataMaps,
}: {
_existingFlatViewMaps: FlatViewMaps;
flatViewToValidate: FlatView;
optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps;
}): Promise<FailedFlatViewValidation> {
optimisticFlatViewMaps,
}: ViewValidationArgs): Promise<FailedFlatEntityValidation<FlatView>> {
const optimisticFlatObjectMetadata =
optimisticFlatObjectMetadataMaps.byId[
flatViewToValidate.objectMetadataId
];
flatObjectMetadataMaps.byId[flatViewToValidate.objectMetadataId];
const errors = [];
@@ -93,10 +94,18 @@ export class FlatViewValidatorService {
});
}
if (isDefined(optimisticFlatViewMaps.byId[flatViewToValidate.id])) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View with same id is already exists`,
userFriendlyMessage: t`View already exists`,
});
}
return {
type: 'create_view',
viewLevelErrors: errors,
failedViewValidationMinimalInformation: {
errors,
flatEntityMinimalInformation: {
id: flatViewToValidate.id,
},
};
@@ -9,8 +9,8 @@ import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/work
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 { 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';
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module';
@Module({
@@ -23,14 +23,16 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
FlatObjectMetadataValidatorService,
WorkspaceMigrationV2FieldActionsBuilderService,
WorkspaceMigrationV2ViewActionsBuilderService,
WorkspaceViewMigrationBuilderV2Service,
WorkspaceViewFieldMigrationBuilderV2Service,
WorkspaceMigrationV2ViewFieldActionsBuilderService,
FlatViewValidatorService,
FlatViewFieldValidatorService,
],
exports: [
WorkspaceMigrationBuilderV2Service,
WorkspaceViewMigrationBuilderV2Service,
WorkspaceViewFieldMigrationBuilderV2Service,
WorkspaceMigrationV2ViewActionsBuilderService,
FlatViewValidatorService,
WorkspaceMigrationV2ViewFieldActionsBuilderService,
FlatViewFieldValidatorService,
],
})
export class WorkspaceMigrationBuilderV2Module {}
@@ -2,7 +2,7 @@ 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 { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.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';
@@ -22,12 +22,11 @@ export class UpdateViewFieldActionHandlerService extends WorkspaceMigrationRunne
const { viewFieldId } = action;
const viewFieldRepository =
queryRunner.manager.getRepository<ViewEntity>(ViewEntity);
queryRunner.manager.getRepository<ViewFieldEntity>(ViewFieldEntity);
await viewFieldRepository.update(
viewFieldId,
fromWorkspaceMigrationUpdateActionToPartialEntity(action),
);
const update = fromWorkspaceMigrationUpdateActionToPartialEntity(action);
await viewFieldRepository.update(viewFieldId, update);
}
async executeForWorkspaceSchema(
@@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Field Resolver - Failing Create Operation - v2 should fail to create view field when non-existent field metadata 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"objectMetadata": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating view field",
"name": "GraphQLError",
}
`;
exports[`View Field Resolver - Failing Create Operation - v2 should fail to create view field when non-existent view metadata 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"objectMetadata": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating view field",
"name": "GraphQLError",
}
`;
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Field Resolver - Failing Delete Operation - v2 should fail to delete view field when non-existent view field 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "View field to delete not found",
"name": "NotFoundError",
}
`;
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Field Resolver - Failing Destroy Operation - v2 should fail to destroy view field when non-existent view field 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "View field to destroy not found",
"name": "NotFoundError",
}
`;
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Field Resolver - Failing Update Operation - v2 should fail to update view field when non-existent view field 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "View field to update not found",
"name": "NotFoundError",
}
`;
@@ -0,0 +1,72 @@
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { faker } from '@faker-js/faker';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
describe('View Field Resolver - Failing Create Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
type CreateViewFieldTestCase = (testSetup: ViewFieldTestSetup) => {
input: CreateViewFieldInput;
};
const createViewFieldTestCases: EachTestingContext<CreateViewFieldTestCase>[] =
[
{
title: 'non-existent field metadata',
context: (testSetup) => ({
input: {
viewId: testSetup.testViewId,
fieldMetadataId: faker.string.uuid(),
},
}),
},
{
title: 'non-existent view metadata',
context: (testSetup) => ({
input: {
viewId: faker.string.uuid(),
fieldMetadataId: testSetup.testFieldMetadataId,
},
}),
},
];
it.each(eachTestingContextFilter(createViewFieldTestCases))(
'should fail to create view field when $title',
async ({ context }) => {
const { input } = context(testSetup);
const response = await createOneCoreViewField({
input,
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
},
);
});
@@ -0,0 +1,61 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
describe('View Field Resolver - Failing Delete Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
type DeleteViewFieldTestCase = {
input: DeleteViewFieldInput;
};
const deleteViewFieldTestCases: EachTestingContext<DeleteViewFieldTestCase>[] =
[
{
title: 'non-existent view field',
context: {
input: {
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
},
},
},
];
it.each(eachTestingContextFilter(deleteViewFieldTestCases))(
'should fail to delete view field when $title',
async ({ context }) => {
const response = await deleteOneCoreViewField({
input: context.input,
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
},
);
});
@@ -0,0 +1,61 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
describe('View Field Resolver - Failing Destroy Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
type DestroyViewFieldTestCase = {
input: DestroyViewFieldInput;
};
const destroyViewFieldTestCases: EachTestingContext<DestroyViewFieldTestCase>[] =
[
{
title: 'non-existent view field',
context: {
input: {
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
},
},
},
];
it.each(eachTestingContextFilter(destroyViewFieldTestCases))(
'should fail to destroy view field when $title',
async ({ context }) => {
const response = await destroyOneCoreViewField({
input: context.input,
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
},
);
});
@@ -0,0 +1,64 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
describe('View Field Resolver - Failing Update Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
type UpdateViewFieldTestCase = {
input: UpdateViewFieldInput;
};
const updateViewFieldTestCases: EachTestingContext<UpdateViewFieldTestCase>[] =
[
{
title: 'non-existent view field',
context: {
input: {
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
update: {
position: 1,
},
},
},
},
];
it.each(eachTestingContextFilter(updateViewFieldTestCases))(
'should fail to update view field when $title',
async ({ context }) => {
const response = await updateOneCoreViewField({
input: context.input,
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
},
);
});
@@ -0,0 +1,146 @@
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
import { assertViewFieldStructure } from 'test/integration/utils/view-test.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
import { isDefined } from 'twenty-shared/utils';
import { type ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
type TestContext = {
viewFieldInput: (testSetup: ViewFieldTestSetup) => CreateViewFieldInput;
expected: Partial<ViewFieldDTO>;
};
describe('View Field Resolver - Successful Create Operations - v2', () => {
let testSetup: ViewFieldTestSetup;
let createdViewFieldId: string | undefined;
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
afterEach(async () => {
if (isDefined(createdViewFieldId)) {
const {
data: { deleteCoreViewField },
} = await deleteOneCoreViewField({
expectToFail: false,
input: {
id: createdViewFieldId,
},
});
expect(deleteCoreViewField.deletedAt).not.toBeNull();
await destroyOneCoreViewField({
expectToFail: false,
input: {
id: createdViewFieldId,
},
});
createdViewFieldId = undefined;
}
});
const successfulTestCases: EachTestingContext<TestContext>[] = [
{
title: 'visible field with position and size',
context: {
viewFieldInput: (testSetup) => ({
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
position: 1,
isVisible: true,
size: 200,
}),
expected: {
position: 1,
isVisible: true,
size: 200,
},
},
},
{
title: 'hidden field with position and size',
context: {
viewFieldInput: (testSetup) => ({
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
position: 2,
isVisible: false,
size: 100,
}),
expected: {
position: 2,
isVisible: false,
size: 100,
},
},
},
{
title: 'field with minimum required properties',
context: {
viewFieldInput: (testSetup) => ({
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
}),
expected: {
position: 0,
isVisible: true,
size: 0,
},
},
},
{
title: 'field with maximum size',
context: {
viewFieldInput: (testSetup) => ({
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
position: 3,
isVisible: true,
size: 1000,
}),
expected: {
position: 3,
isVisible: true,
size: 1000,
},
},
},
];
test.each(eachTestingContextFilter(successfulTestCases))(
'Create $title',
async ({ context: { viewFieldInput, expected } }) => {
const response = await createOneCoreViewField({
input: viewFieldInput(testSetup),
expectToFail: false,
});
expect(response.errors).toBeUndefined();
expect(response.data.createCoreViewField).toBeDefined();
createdViewFieldId = response.data.createCoreViewField.id;
assertViewFieldStructure(response.data.createCoreViewField, {
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
...expected,
});
},
);
});
@@ -0,0 +1,95 @@
import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
export type ViewFieldTestSetup = {
testViewId: string;
testObjectMetadataId: string;
testFieldMetadataId: string;
};
export const setupViewFieldTestV2 = async (): Promise<ViewFieldTestSetup> => {
await updateFeatureFlag({
expectToFail: false,
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
value: true,
});
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'myFieldTestObjectV2',
namePlural: 'myFieldTestObjectsV2',
labelSingular: 'My Field Test Object v2',
labelPlural: 'My Field Test Objects v2',
icon: 'Icon123',
},
});
const {
data: {
createOneField: { id: fieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId,
isLabelSyncedWithName: true,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
const view = await createTestViewWithGraphQL({
name: 'Test View for Fields',
objectMetadataId,
});
return {
testViewId: view.id,
testObjectMetadataId: objectMetadataId,
testFieldMetadataId: fieldMetadataId,
};
};
export const cleanupViewFieldTestV2 = async (
objectMetadataId: string,
): Promise<void> => {
try {
await updateOneObjectMetadata({
input: {
idToUpdate: objectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: objectMetadataId },
});
} finally {
await updateFeatureFlag({
expectToFail: false,
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
value: false,
});
}
};
@@ -1,33 +1,34 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { createViewFieldOperationFactory } from 'test/integration/graphql/utils/create-view-field-operation-factory.util';
import { deleteViewFieldOperationFactory } from 'test/integration/graphql/utils/delete-view-field-operation-factory.util';
import { destroyViewFieldOperationFactory } from 'test/integration/graphql/utils/destroy-view-field-operation-factory.util';
import { findViewFieldsOperationFactory } from 'test/integration/graphql/utils/find-view-fields-operation-factory.util';
import {
assertGraphQLErrorResponse,
assertGraphQLSuccessfulResponse,
} from 'test/integration/graphql/utils/graphql-test-assertions.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { updateViewFieldOperationFactory } from 'test/integration/graphql/utils/update-view-field-operation-factory.util';
import {
createViewFieldData,
updateViewFieldData,
} from 'test/integration/graphql/utils/view-data-factory.util';
import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import {
assertViewFieldStructure,
cleanupViewRecords,
} from 'test/integration/utils/view-test.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
generateViewFieldExceptionMessage,
ViewFieldExceptionMessageKey,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
describe('View Field Resolver', () => {
let testViewId: string;
@@ -113,11 +114,11 @@ describe('View Field Resolver', () => {
size: 150,
fieldMetadataId: testFieldMetadataId,
});
const createOperation = createViewFieldOperationFactory({
data: fieldData,
});
await makeGraphqlAPIRequest(createOperation);
await createOneCoreViewField({
input: fieldData as CreateViewFieldInput,
expectToFail: false,
});
const getOperation = findViewFieldsOperationFactory({
viewId: testViewId,
@@ -145,11 +146,13 @@ describe('View Field Resolver', () => {
fieldMetadataId: testFieldMetadataId,
});
const operation = createViewFieldOperationFactory({ data: fieldData });
const response = await makeGraphqlAPIRequest(operation);
const response = await createOneCoreViewField({
input: fieldData as CreateViewFieldInput,
expectToFail: false,
});
assertGraphQLSuccessfulResponse(response);
assertViewFieldStructure(response.body.data.createCoreViewField, {
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
assertViewFieldStructure(response.data.createCoreViewField, {
fieldMetadataId: testFieldMetadataId,
position: 1,
isVisible: true,
@@ -167,11 +170,13 @@ describe('View Field Resolver', () => {
viewId: testViewId,
};
const operation = createViewFieldOperationFactory({ data: fieldData });
const response = await makeGraphqlAPIRequest(operation);
const response = await createOneCoreViewField({
input: fieldData,
expectToFail: false,
});
assertGraphQLSuccessfulResponse(response);
assertViewFieldStructure(response.body.data.createCoreViewField, {
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
assertViewFieldStructure(response.data.createCoreViewField, {
fieldMetadataId: testFieldMetadataId,
position: 2,
isVisible: false,
@@ -189,27 +194,27 @@ describe('View Field Resolver', () => {
size: 150,
fieldMetadataId: testFieldMetadataId,
});
const createOperation = createViewFieldOperationFactory({
data: fieldData,
const createResponse = await createOneCoreViewField({
input: fieldData as CreateViewFieldInput,
expectToFail: false,
});
const createResponse = await makeGraphqlAPIRequest(createOperation);
const viewField = createResponse.body.data.createCoreViewField;
const viewField = createResponse.data.createCoreViewField;
const updateInput = updateViewFieldData({
position: 5,
isVisible: false,
size: 300,
});
const updateOperation = updateViewFieldOperationFactory({
const response = await updateOneCoreViewField({
input: {
id: viewField.id,
update: updateInput,
},
expectToFail: false,
});
const response = await makeGraphqlAPIRequest(updateOperation);
assertGraphQLSuccessfulResponse(response);
expect(response.body.data.updateCoreViewField).toMatchObject({
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
expect(response.data.updateCoreViewField).toMatchObject({
id: viewField.id,
position: 5,
isVisible: false,
@@ -218,18 +223,18 @@ describe('View Field Resolver', () => {
});
it('should throw an error when updating non-existent view field', async () => {
const operation = updateViewFieldOperationFactory({
const response = await updateOneCoreViewField({
input: {
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
update: {
position: 1,
},
},
expectToFail: true,
});
const response = await makeGraphqlAPIRequest(operation);
assertGraphQLErrorResponse(
response,
{ body: response, status: 200 },
ErrorCode.NOT_FOUND,
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
@@ -244,31 +249,31 @@ describe('View Field Resolver', () => {
const fieldData = createViewFieldData(testViewId, {
fieldMetadataId: testFieldMetadataId,
});
const createOperation = createViewFieldOperationFactory({
data: fieldData,
const createResponse = await createOneCoreViewField({
input: fieldData as CreateViewFieldInput,
expectToFail: false,
});
const createResponse = await makeGraphqlAPIRequest(createOperation);
const viewField = createResponse.body.data.createCoreViewField;
const viewField = createResponse.data.createCoreViewField;
const deleteOperation = deleteViewFieldOperationFactory({
viewFieldId: viewField.id,
const response = await deleteOneCoreViewField({
input: { id: viewField.id },
expectToFail: false,
});
const response = await makeGraphqlAPIRequest(deleteOperation);
assertGraphQLSuccessfulResponse(response);
expect(response.body.data.deleteCoreViewField).toMatchObject({
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
expect(response.data.deleteCoreViewField).toMatchObject({
id: viewField.id,
});
});
it('should throw an error when deleting non-existent view field', async () => {
const operation = deleteViewFieldOperationFactory({
viewFieldId: TEST_NOT_EXISTING_VIEW_FIELD_ID,
const response = await deleteOneCoreViewField({
input: { id: TEST_NOT_EXISTING_VIEW_FIELD_ID },
expectToFail: true,
});
const response = await makeGraphqlAPIRequest(operation);
assertGraphQLErrorResponse(
response,
{ body: response, status: 200 },
ErrorCode.NOT_FOUND,
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
@@ -283,31 +288,35 @@ describe('View Field Resolver', () => {
const fieldData = createViewFieldData(testViewId, {
fieldMetadataId: testFieldMetadataId,
});
const createOperation = createViewFieldOperationFactory({
data: fieldData,
const createResponse = await createOneCoreViewField({
input: fieldData as CreateViewFieldInput,
expectToFail: false,
});
const createResponse = await makeGraphqlAPIRequest(createOperation);
const viewField = createResponse.body.data.createCoreViewField;
const viewField = createResponse.data.createCoreViewField;
const destroyOperation = destroyViewFieldOperationFactory({
viewFieldId: viewField.id,
const response = await destroyOneCoreViewField({
input: {
id: viewField.id,
},
expectToFail: false,
});
const response = await makeGraphqlAPIRequest(destroyOperation);
assertGraphQLSuccessfulResponse(response);
expect(response.body.data.destroyCoreViewField).toMatchObject({
assertGraphQLSuccessfulResponse({ body: response, status: 200 });
expect(response.data.destroyCoreViewField).toMatchObject({
id: viewField.id,
});
});
it('should throw an error when destroying non-existent view field', async () => {
const operation = destroyViewFieldOperationFactory({
viewFieldId: TEST_NOT_EXISTING_VIEW_FIELD_ID,
const response = await destroyOneCoreViewField({
input: {
id: TEST_NOT_EXISTING_VIEW_FIELD_ID,
},
expectToFail: true,
});
const response = await makeGraphqlAPIRequest(operation);
assertGraphQLErrorResponse(
response,
{ body: response, status: 200 },
ErrorCode.NOT_FOUND,
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
@@ -1,21 +0,0 @@
import gql from 'graphql-tag';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const deleteViewFieldOperationFactory = ({
viewFieldId,
gqlFields = VIEW_FIELD_GQL_FIELDS,
}: {
gqlFields?: string;
viewFieldId: string;
}) => ({
query: gql`
mutation DeleteCoreViewField($input: DeleteViewFieldInput!) {
deleteCoreViewField(input: $input) {
${gqlFields}
}
}
`,
variables: {
input: { id: viewFieldId },
},
});
@@ -1,21 +0,0 @@
import gql from 'graphql-tag';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const destroyViewFieldOperationFactory = ({
viewFieldId,
gqlFields = VIEW_FIELD_GQL_FIELDS,
}: {
gqlFields?: string;
viewFieldId: string;
}) => ({
query: gql`
mutation DestroyCoreViewField($input: DestroyViewFieldInput!) {
destroyCoreViewField(input: $input){
${gqlFields}
}
}
`,
variables: {
input: { id: viewFieldId },
},
});
@@ -5,7 +5,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -15,10 +15,12 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re
"value": "toto",
},
],
"id": Any<String>,
"name": "toto",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "toto",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
{
"errors": [
@@ -29,21 +31,25 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re
"value": "tata",
},
],
"id": Any<String>,
"name": "tata",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "tata",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 2 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 2,
"invalidObjects": 0,
"totalErrors": 2,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 2 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating fields",
"name": "GraphQLError",
@@ -55,7 +61,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -65,21 +71,25 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re
"value": "fieldNamePersonForMorphRelationSecondId",
},
],
"id": Any<String>,
"name": "fieldNamePersonForMorphRelationSecondId",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "fieldNamePersonForMorphRelationSecondId",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating fields",
"name": "GraphQLError",
@@ -91,7 +101,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -100,18 +110,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"userFriendlyMessage": "Morph relation creation payloads must only contain relation to other object metadata",
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -123,7 +136,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -132,18 +145,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"userFriendlyMessage": "Morph relation creation payloads must only contain relation to the same object metadata",
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -155,7 +171,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -171,21 +187,25 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"value": "",
},
],
"id": Any<String>,
"name": "",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating fields",
"name": "GraphQLError",
@@ -197,7 +217,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -207,21 +227,25 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating fields",
"name": "GraphQLError",
@@ -233,7 +257,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -243,21 +267,25 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"value": "name",
},
],
"id": Any<String>,
"name": "name",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "name",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating fields",
"name": "GraphQLError",
@@ -269,7 +297,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -285,21 +313,25 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"value": "",
},
],
"id": Any<String>,
"name": "",
"objectMetadataId": Any<String>,
"operation": "create_field",
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "",
"objectMetadataId": Any<String>,
},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating fields",
"name": "GraphQLError",
@@ -311,7 +343,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -328,18 +360,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
],
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -351,7 +386,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -368,18 +403,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
],
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -391,7 +429,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -407,18 +445,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
],
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -430,7 +471,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -439,18 +480,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"userFriendlyMessage": "At least one relation is require",
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -462,7 +506,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -471,18 +515,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"userFriendlyMessage": "Morph relation creation payloads must have the same relation type",
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -494,7 +541,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -503,18 +550,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"userFriendlyMessage": "Morph relation creation payloads must have the same relation type",
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -526,7 +576,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -538,18 +588,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
],
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -561,7 +614,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -571,18 +624,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"value": null,
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -594,7 +650,7 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
"objectMetadata": [
{
"errors": [
{
@@ -603,18 +659,21 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou
"userFriendlyMessage": "Relation creation payload is required",
},
],
"operation": "create_field",
"flatEntityMinimalInformation": {},
"type": "create_field",
},
],
"objectMetadata": [],
"view": [],
"viewField": [],
},
"message": "Validation failed for 0 object(s) and 1 field(s)",
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidFields": 1,
"invalidObjects": 0,
"totalErrors": 1,
"invalidObjectMetadatas": 0,
"invalidViewFields": 0,
"invalidViews": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 1 field(s)",
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating field",
"name": "GraphQLError",
@@ -1,11 +1,11 @@
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
@@ -37,7 +37,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
data: {
createOneObject: { id: aId },
},
} = await forceCreateOneObjectMetadata({
} = await createOneObjectMetadata({
input: {
nameSingular: 'opportunityForRenameSecond',
namePlural: 'opportunitiesForRenameSecond',
@@ -52,7 +52,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
data: {
createOneObject: { id: bId },
},
} = await forceCreateOneObjectMetadata({
} = await createOneObjectMetadata({
input: {
nameSingular: 'personForRenameSecond',
namePlural: 'peopleForRenameSecond',
@@ -67,7 +67,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
data: {
createOneObject: { id: cId },
},
} = await forceCreateOneObjectMetadata({
} = await createOneObjectMetadata({
input: {
nameSingular: 'companyForRenameSecond',
namePlural: 'companiesForRenameSecond',
@@ -1,17 +1,13 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
type CreateViewFieldOperationFactoryParams = {
gqlFields?: string;
data?: Partial<ViewFieldEntity>;
};
export const createViewFieldOperationFactory = ({
export const createCoreViewFieldQueryFactory = ({
gqlFields = VIEW_FIELD_GQL_FIELDS,
data = {},
}: CreateViewFieldOperationFactoryParams = {}) => ({
input,
}: PerformMetadataQueryParams<CreateViewFieldInput>) => ({
query: gql`
mutation CreateCoreViewField($input: CreateViewFieldInput!) {
createCoreViewField(input: $input) {
@@ -20,6 +16,6 @@ export const createViewFieldOperationFactory = ({
}
`,
variables: {
input: data,
input,
},
});
@@ -0,0 +1,40 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { createCoreViewFieldQueryFactory } from 'test/integration/metadata/suites/view-field/utils/create-core-view-field-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
export const createOneCoreViewField = async ({
input,
gqlFields,
expectToFail,
}: PerformMetadataQueryParams<CreateViewFieldInput>): CommonResponseBody<{
createCoreViewField: ViewFieldEntity;
}> => {
const graphqlOperation = createCoreViewFieldQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View Field creation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View Field creation has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
export const deleteCoreViewFieldQueryFactory = ({
gqlFields = VIEW_FIELD_GQL_FIELDS,
input,
}: PerformMetadataQueryParams<DeleteViewFieldInput>) => ({
query: gql`
mutation DeleteCoreViewField($input: DeleteViewFieldInput!) {
deleteCoreViewField(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
@@ -0,0 +1,40 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { deleteCoreViewFieldQueryFactory } from 'test/integration/metadata/suites/view-field/utils/delete-core-view-field-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
export const deleteOneCoreViewField = async ({
input,
gqlFields,
expectToFail,
}: PerformMetadataQueryParams<DeleteViewFieldInput>): CommonResponseBody<{
deleteCoreViewField: ViewFieldEntity;
}> => {
const graphqlOperation = deleteCoreViewFieldQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View Field deletion should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View Field deletion has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
export const destroyCoreViewFieldQueryFactory = ({
gqlFields = VIEW_FIELD_GQL_FIELDS,
input,
}: PerformMetadataQueryParams<DestroyViewFieldInput>) => ({
query: gql`
mutation DestroyCoreViewField($input: DestroyViewFieldInput!) {
destroyCoreViewField(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
@@ -0,0 +1,40 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { destroyCoreViewFieldQueryFactory } from 'test/integration/metadata/suites/view-field/utils/destroy-core-view-field-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
export const destroyOneCoreViewField = async ({
input,
gqlFields,
expectToFail,
}: PerformMetadataQueryParams<DestroyViewFieldInput>): CommonResponseBody<{
destroyCoreViewField: ViewFieldEntity;
}> => {
const graphqlOperation = destroyCoreViewFieldQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View Field destruction should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View Field destruction has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -1,15 +1,13 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
export const updateViewFieldOperationFactory = ({
export const updateCoreViewFieldQueryFactory = ({
gqlFields = VIEW_FIELD_GQL_FIELDS,
input,
}: {
gqlFields?: string;
input: UpdateViewFieldInput;
}) => ({
}: PerformMetadataQueryParams<UpdateViewFieldInput>) => ({
query: gql`
mutation UpdateCoreViewField($input: UpdateViewFieldInput!) {
updateCoreViewField(input: $input) {
@@ -0,0 +1,40 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { updateCoreViewFieldQueryFactory } from 'test/integration/metadata/suites/view-field/utils/update-core-view-field-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
export const updateOneCoreViewField = async ({
input,
gqlFields,
expectToFail,
}: PerformMetadataQueryParams<UpdateViewFieldInput>): CommonResponseBody<{
updateCoreViewField: ViewFieldEntity;
}> => {
const graphqlOperation = updateCoreViewFieldQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View Field update should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View Field update has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input';
export const createCoreViewQueryFactory = ({
gqlFields = VIEW_GQL_FIELDS,
input,
}: PerformMetadataQueryParams<CreateViewInput>) => ({
query: gql`
mutation CreateCoreView($input: CreateViewInput!) {
createCoreView(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
@@ -0,0 +1,41 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { createCoreViewQueryFactory } from './create-core-view-query-factory.util';
export const createOneCoreView = async ({
input,
gqlFields,
expectToFail,
}: PerformMetadataQueryParams<CreateViewInput>): CommonResponseBody<{
createCoreView: ViewEntity;
}> => {
const graphqlOperation = createCoreViewQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View creation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View creation has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};