View field create many mutation (#15576)

# Introduction
When creating a view with v2 flag activated in production result in race
condition due to request being slow and //.
That's why we're introducing a batch create on view field here
closing https://github.com/twentyhq/core-team-issues/issues/1836

## In v2
- batch create view field endpoint is available
- frontend will target the new endpoint

## In v1
- batch create view field endpoint is not available
- frontend will stick to old fake batch view field creation loop

## Polish
- use persist view field is quite verbose as contains two data model we
could aim to create an update many view fields in order to standardize
new pattern
This commit is contained in:
Paul Rastoin
2025-11-04 11:36:08 +01:00
committed by GitHub
parent 106c33abec
commit a3dc6e5c59
17 changed files with 855 additions and 29 deletions
@@ -5,13 +5,15 @@ import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/
import { type CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
export type FromCreateViewFieldInputToFlatViewFieldToCreateArgs = {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
};
export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
createViewFieldInput: rawCreateViewFieldInput,
workspaceId,
}: {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
}): FlatViewField => {
}: FromCreateViewFieldInputToFlatViewFieldToCreateArgs): FlatViewField => {
const { fieldMetadataId, viewId, ...createViewFieldInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateViewFieldInput,
@@ -12,6 +12,10 @@ import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dt
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
@@ -91,6 +95,31 @@ export class ViewFieldResolver {
});
}
@Mutation(() => [ViewFieldDTO])
async createManyCoreViewFields(
@Args('inputs', { type: () => [CreateViewFieldInput] })
createViewFieldInputs: CreateViewFieldInput[],
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO[]> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (!isWorkspaceMigrationV2Enabled) {
throw new ViewFieldException(
'Not implemented in v1, please active IS_WORKSPACE_MIGRATION_V2_ENABLED',
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
);
}
return await this.viewFieldV2Service.createMany({
createViewFieldInputs,
workspaceId,
});
}
@Mutation(() => ViewFieldDTO)
async deleteCoreViewField(
@Args('input') deleteViewFieldInput: DeleteViewFieldInput,
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util';
import { fromDeleteViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/metadata-modules/flat-view-field/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util';
import { fromDestroyViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/metadata-modules/flat-view-field/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util';
@@ -14,6 +15,10 @@ import { DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dto
import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/destroy-view-field.input';
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
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';
@@ -31,6 +36,32 @@ export class ViewFieldV2Service {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const [createdViewField] = await this.createMany({
workspaceId,
createViewFieldInputs: [createViewFieldInput],
});
if (!isDefined(createdViewField)) {
throw new ViewFieldException(
'Failed to create view field',
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
);
}
return createdViewField;
}
async createMany({
createViewFieldInputs,
workspaceId,
}: {
createViewFieldInputs: CreateViewFieldInput[];
workspaceId: string;
}): Promise<ViewFieldDTO[]> {
if (createViewFieldInputs.length === 0) {
return [];
}
const {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewMaps,
@@ -48,11 +79,13 @@ export class ViewFieldV2Service {
},
);
const flatViewFieldToCreate =
fromCreateViewFieldInputToFlatViewFieldToCreate({
createViewFieldInput,
workspaceId,
});
const flatViewFieldsToCreate = createViewFieldInputs.map(
(createViewFieldInput) =>
fromCreateViewFieldInputToFlatViewFieldToCreate({
createViewFieldInput,
workspaceId,
}),
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -60,7 +93,7 @@ export class ViewFieldV2Service {
fromToAllFlatEntityMaps: {
flatViewFieldMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatViewFieldMaps,
flatEntityToCreate: [flatViewFieldToCreate],
flatEntityToCreate: flatViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
}),
@@ -80,7 +113,7 @@ export class ViewFieldV2Service {
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating view field',
'Multiple validation errors occurred while creating view fields',
);
}
@@ -92,8 +125,8 @@ export class ViewFieldV2Service {
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFieldToCreate.id,
return findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatViewFieldsToCreate.map((el) => el.id),
flatEntityMaps: recomputedExistingFlatViewFieldMaps,
});
}