[CoreViewField BREAKING_CHANGES] Refactor view field service v2 and resolver (#14396)

## Introduction

### Twenty-sever

Standardizing resolver input and transpilation models + return type on
destroy and delete
~~Finally~~ Did plug everything under a feature flag and add coverage
Next will do same for the view resolver and service v2

### Twenty-front
Refactored view field service in order to use codegenerated strictly
typed mutations and adapt to new api contract
This commit is contained in:
Paul Rastoin
2025-09-11 17:25:36 +02:00
committed by GitHub
parent d9a6e7e0b1
commit c8bfbf00d7
40 changed files with 940 additions and 397 deletions
@@ -0,0 +1,14 @@
import { CustomException } from 'src/utils/custom-exception';
export class FlatEntityMapsException extends CustomException {
code: FlatEntityMapsExceptionCode;
constructor(message: string, code: FlatEntityMapsExceptionCode) {
super(message, code);
}
}
export enum FlatEntityMapsExceptionCode {
ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
}
@@ -1,14 +0,0 @@
import { CustomException } from 'src/utils/custom-exception';
export class FlatEntityException extends CustomException {
code: FlatEntityExceptionCode;
constructor(message: string, code: FlatEntityExceptionCode) {
super(message, code);
}
}
export enum FlatEntityExceptionCode {
ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
}
@@ -1,9 +1,9 @@
import { isDefined } from 'class-validator';
import {
FlatEntityException,
FlatEntityExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity.exception';
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
@@ -17,9 +17,9 @@ export const addFlatEntityToFlatEntityMapsOrThrow = <T extends FlatEntity>({
flatEntityMaps,
}: AddFlatEntityToFlatEntityMapsOrThrowArgs<T>): FlatEntityMaps<T> => {
if (isDefined(flatEntityMaps.byId[flatEntity.id])) {
throw new FlatEntityException(
throw new FlatEntityMapsException(
'addFlatEntityToFlatEntityMapsOrThrow: flat entity to add already exists',
FlatEntityExceptionCode.ENTITY_ALREADY_EXISTS,
FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
);
}
@@ -1,11 +1,11 @@
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import {
FlatEntityException,
FlatEntityExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity.exception';
export type DeleteFlatEntityFromFlatEntityMapsOrThrowArgs<
T extends FlatEntity,
@@ -21,9 +21,9 @@ export const deleteFlatEntityFromFlatEntityMapsOrThrow = <
entityToDeleteId,
}: DeleteFlatEntityFromFlatEntityMapsOrThrowArgs<T>): FlatEntityMaps<T> => {
if (!isDefined(flatEntityMaps.byId[entityToDeleteId])) {
throw new FlatEntityException(
throw new FlatEntityMapsException(
'deleteFlatEntityFromFlatEntityMapsOrThrow: entity to delete not found',
FlatEntityExceptionCode.ENTITY_NOT_FOUND,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
@@ -0,0 +1,30 @@
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
export const findFlatEntityByIdInFlatEntityMapsOrThrow = <
T extends FlatEntity,
>({
flatEntityMaps,
flatEntityId,
}: {
flatEntityMaps: FlatEntityMaps<T>;
flatEntityId: string;
}): T => {
const flatEntity = flatEntityMaps.byId[flatEntityId];
if (!isDefined(flatEntity)) {
throw new FlatEntityMapsException(
t`Could not find flat entity in maps`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return flatEntity;
};
@@ -75,7 +75,7 @@ export class ViewFieldController {
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewFieldInput,
@Body() input: UpdateViewFieldInput['update'],
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFieldEntity> {
const updatedViewField = await this.viewFieldService.update(
@@ -1,28 +1,48 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsNumber,
IsOptional,
IsUUID,
} from 'class-validator';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateViewFieldInput {
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsUUID()
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@IsUUID()
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@IsOptional()
@IsBoolean()
@Field({ nullable: true, defaultValue: true })
isVisible?: boolean;
@IsOptional()
@IsNumber()
@Field({ nullable: true, defaultValue: 0 })
size?: number;
@IsOptional()
@IsNumber()
@Field({ nullable: true, defaultValue: 0 })
position?: number;
@IsOptional()
@IsEnum(AggregateOperations)
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
}
@@ -0,0 +1,15 @@
import { InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class DeleteViewFieldInput {
@IDField(() => UUIDScalarType, {
description: 'The id of the view field to delete.',
})
@IsUUID()
id: string;
}
@@ -0,0 +1,15 @@
import { InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class DestroyViewFieldInput {
@IDField(() => UUIDScalarType, {
description: 'The id of the view field to destroy.',
})
@IsUUID()
id: string;
}
@@ -1,18 +1,55 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsUUID,
ValidateNested,
} from 'class-validator';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class UpdateViewFieldInput {
class UpdateViewFieldInputUpdates {
@IsOptional()
@IsBoolean()
@Field({ nullable: true })
isVisible?: boolean;
@IsOptional()
@IsNumber()
@Field({ nullable: true })
size?: number;
@IsOptional()
@IsNumber()
@Field({ nullable: true })
position?: number;
@IsOptional()
@IsEnum(AggregateOperations)
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
}
@InputType()
export class UpdateViewFieldInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the view field to update',
})
id: string;
@Type(() => UpdateViewFieldInputUpdates)
@ValidateNested()
@Field(() => UpdateViewFieldInputUpdates, {
description: 'The view field to update',
})
update: UpdateViewFieldInputUpdates;
}
@@ -0,0 +1,37 @@
import { v4 } from 'uuid';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
createViewFieldInput: rawCreateViewFieldInput,
workspaceId,
}: {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
}): FlatViewField => {
const { fieldMetadataId, viewId, ...createViewFieldInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateViewFieldInput,
['aggregateOperation', 'fieldMetadataId', 'id', 'viewId'],
);
const createdAt = new Date();
const viewFieldId = createViewFieldInput.id ?? v4();
return {
id: viewFieldId,
fieldMetadataId,
viewId,
workspaceId,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: viewFieldId,
isVisible: createViewFieldInput.isVisible ?? true,
size: createViewFieldInput.size ?? 0,
position: createViewFieldInput.position ?? 0,
aggregateOperation: createViewFieldInput.aggregateOperation ?? null,
};
};
@@ -0,0 +1,40 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
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';
export const fromDeleteViewFieldInputToFlatViewFieldOrThrow = ({
deleteViewFieldInput: rawDeleteViewFieldInput,
flatViewFieldMaps,
}: {
deleteViewFieldInput: DeleteViewFieldInput;
flatViewFieldMaps: FlatViewFieldMaps;
}): FlatViewField => {
const { id: viewFieldId } = extractAndSanitizeObjectStringFields(
rawDeleteViewFieldInput,
['id'],
);
const existingFlatViewFieldToDelete = flatViewFieldMaps.byId[viewFieldId];
if (!isDefined(existingFlatViewFieldToDelete)) {
throw new ViewFieldException(
t`View field to delete not found`,
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
return {
...existingFlatViewFieldToDelete,
deletedAt: new Date(),
};
};
@@ -0,0 +1,37 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
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';
export const fromDestroyViewFieldInputToFlatViewFieldOrThrow = ({
destroyViewFieldInput: rawDeleteDestroyViewInput,
flatViewFieldMaps,
}: {
destroyViewFieldInput: DestroyViewFieldInput;
flatViewFieldMaps: FlatViewFieldMaps;
}): FlatViewField => {
const { id: viewFieldId } = extractAndSanitizeObjectStringFields(
rawDeleteDestroyViewInput,
['id'],
);
const existingFlatViewFieldToDestroy = flatViewFieldMaps.byId[viewFieldId];
if (!isDefined(existingFlatViewFieldToDestroy)) {
throw new ViewFieldException(
t`View field to destroy not found`,
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
return existingFlatViewFieldToDestroy;
};
@@ -1,27 +0,0 @@
import { v4 } from 'uuid';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const fromPartialFlatViewFieldToFlatViewFieldWithDefault = (
partialFlatViewField: Partial<FlatViewField>,
): FlatViewField => {
const createdAt = new Date();
const viewFieldId = partialFlatViewField.id ?? v4();
return {
...partialFlatViewField,
id: viewFieldId,
fieldMetadataId: partialFlatViewField.fieldMetadataId ?? '',
isVisible: partialFlatViewField.isVisible ?? true,
size: partialFlatViewField.size ?? 0,
position: partialFlatViewField.position ?? 0,
aggregateOperation: partialFlatViewField.aggregateOperation ?? null,
viewId: partialFlatViewField.viewId ?? '',
workspaceId: partialFlatViewField.workspaceId ?? '',
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier:
partialFlatViewField.universalIdentifier ?? viewFieldId,
};
};
@@ -0,0 +1,50 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
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 { 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';
export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({
updateViewFieldInput: rawUpdateViewFieldInput,
flatViewFieldMaps,
}: {
updateViewFieldInput: UpdateViewFieldInput;
flatViewFieldMaps: FlatViewFieldMaps;
}): FlatViewField => {
const { id: viewFieldToUpdateId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateViewFieldInput,
['id'],
);
const existingFlatViewFieldToUpdate =
flatViewFieldMaps.byId[viewFieldToUpdateId];
if (!isDefined(existingFlatViewFieldToUpdate)) {
throw new ViewFieldException(
t`View field to update not found`,
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdateViewFieldInput.update,
FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE,
);
return mergeUpdateInExistingRecord({
existing: existingFlatViewFieldToUpdate,
properties: FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE,
update: updatedEditableFieldProperties,
});
};
@@ -1,12 +1,15 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { 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 { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFieldV2Service } from 'src/engine/core-modules/view/services/view-field-v2.service';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -17,7 +20,11 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFieldResolver {
constructor(private readonly viewFieldService: ViewFieldService) {}
constructor(
private readonly viewFieldService: ViewFieldService,
private readonly featureFlagService: FeatureFlagService,
private readonly viewFieldV2Service: ViewFieldV2Service,
) {}
@Query(() => [ViewFieldDTO])
async getCoreViewFields(
@@ -37,47 +44,101 @@ export class ViewFieldResolver {
@Mutation(() => ViewFieldDTO)
async updateCoreViewField(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewFieldInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFieldEntity> {
return this.viewFieldService.update(id, workspace.id, input);
@Args('input') updateViewFieldInput: UpdateViewFieldInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.updateOne({
updateViewFieldInput,
workspaceId,
});
}
return this.viewFieldService.update(
updateViewFieldInput.id,
workspaceId,
updateViewFieldInput.update,
);
}
@Mutation(() => ViewFieldDTO)
async createCoreViewField(
@Args('input') input: CreateViewFieldInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFieldEntity> {
@Args('input') createViewFieldInput: CreateViewFieldInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.createOne({
createViewFieldInput,
workspaceId,
});
}
return this.viewFieldService.create({
...input,
workspaceId: workspace.id,
...createViewFieldInput,
workspaceId,
});
}
@Mutation(() => Boolean)
@Mutation(() => ViewFieldDTO)
async deleteCoreViewField(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
@Args('input') deleteViewFieldInput: DeleteViewFieldInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.deleteOne({
deleteViewFieldInput,
workspaceId,
});
}
const deletedViewField = await this.viewFieldService.delete(
id,
workspace.id,
deleteViewFieldInput.id,
workspaceId,
);
return isDefined(deletedViewField);
return deletedViewField;
}
@Mutation(() => Boolean)
@Mutation(() => ViewFieldDTO)
async destroyCoreViewField(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
@Args('input') destroyViewFieldInput: DestroyViewFieldInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.destroyOne({
destroyViewFieldInput,
workspaceId,
});
}
const deletedViewField = await this.viewFieldService.destroy(
id,
workspace.id,
destroyViewFieldInput.id,
workspaceId,
);
return isDefined(deletedViewField);
return deletedViewField;
}
}
@@ -150,19 +150,6 @@ describe('ViewFieldService', () => {
size: 100,
};
it('should create a view field successfully', async () => {
jest.spyOn(viewFieldRepository, 'create').mockReturnValue(mockViewField);
jest.spyOn(viewFieldRepository, 'save').mockResolvedValue(mockViewField);
const result = await viewFieldService.create(validViewFieldData);
expect(viewFieldRepository.create).toHaveBeenCalledWith(
validViewFieldData,
);
expect(viewFieldRepository.save).toHaveBeenCalledWith(mockViewField);
expect(result).toEqual(mockViewField);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewFieldData, workspaceId: undefined };
@@ -308,7 +295,7 @@ describe('ViewFieldService', () => {
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFieldRepository.delete).toHaveBeenCalledWith(id);
expect(result).toEqual(true);
expect(result).toBeDefined();
});
});
});
@@ -1,24 +1,24 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { Equal, Repository } from 'typeorm';
import { v4 } from 'uuid';
import { 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 { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
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 {
ViewFieldException,
ViewFieldExceptionCode,
ViewFieldExceptionMessageKey,
generateViewFieldExceptionMessage,
generateViewFieldUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { fromPartialFlatViewFieldToFlatViewFieldWithDefault } from 'src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util';
import { 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';
@@ -32,7 +32,7 @@ export class ViewFieldV2Service {
) {}
// TODO: move to cache service
private async getExistingFlatViewFieldMaps(
private async getExistingFlatViewFieldMapsFromCache(
workspaceId: string,
): Promise<FlatViewFieldMaps> {
const existingViewFields = await this.viewFieldRepository.find({
@@ -56,38 +56,26 @@ export class ViewFieldV2Service {
return flatViewFieldMaps;
}
async createOne(
viewFieldData: Partial<ViewFieldEntity>,
): Promise<ViewFieldEntity> {
if (!isDefined(viewFieldData.workspaceId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
async createOne({
createViewFieldInput,
workspaceId,
}: {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const existingFlatViewFieldMaps = await this.getExistingFlatViewFieldMaps(
viewFieldData.workspaceId,
);
const flatViewFieldFromCreateInput =
fromPartialFlatViewFieldToFlatViewFieldWithDefault({
...viewFieldData,
universalIdentifier: viewFieldData.universalIdentifier ?? v4(),
const flatViewFieldToCreate =
fromCreateViewFieldInputToFlatViewFieldToCreate({
createViewFieldInput,
workspaceId,
});
const toFlatViewFieldMaps: FlatViewFieldMaps =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFieldFromCreateInput,
flatEntityMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewFieldToCreate,
flatEntityMaps: existingFlatViewFieldMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
@@ -102,7 +90,7 @@ export class ViewFieldV2Service {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId: viewFieldData.workspaceId,
workspaceId,
},
);
@@ -112,55 +100,35 @@ export class ViewFieldV2Service {
);
}
const [createdViewField] = await this.viewFieldRepository.find({
where: {
id: flatViewFieldFromCreateInput.id,
},
});
const recomputedExistingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
return createdViewField;
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFieldToCreate.id,
flatEntityMaps: recomputedExistingFlatViewFieldMaps,
});
}
async updateOne(
id: string,
workspaceId: string,
updateData: Partial<ViewFieldEntity>,
): Promise<ViewFieldEntity> {
async updateOne({
updateViewFieldInput,
workspaceId,
}: {
workspaceId: string;
updateViewFieldInput: UpdateViewFieldInput;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMaps(workspaceId);
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const existingViewField = existingFlatViewFieldMaps.byId[id];
if (!isDefined(existingViewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const existingViewFieldToUpdate = removePropertiesFromRecord(
{
...existingViewField,
...updateData,
},
VIEW_FIELD_ENTITY_RELATION_PROPERTIES,
);
const flatViewFieldFromUpdateInput =
fromPartialFlatViewFieldToFlatViewFieldWithDefault({
...existingViewFieldToUpdate,
universalIdentifier:
existingViewFieldToUpdate.universalIdentifier ?? '',
const optimisticallyUpdatedFlatView =
fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow({
flatViewFieldMaps: existingFlatViewFieldMaps,
updateViewFieldInput,
});
const toFlatViewFieldMaps: FlatViewFieldMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatViewFieldFromUpdateInput,
flatEntityMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatView,
flatEntityMaps: existingFlatViewFieldMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
@@ -185,37 +153,89 @@ export class ViewFieldV2Service {
);
}
const [updatedViewField] = await this.viewFieldRepository.find({
where: {
id: Equal(id),
},
});
const recomputedExistingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
return updatedViewField;
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatView.id,
flatEntityMaps: recomputedExistingFlatViewFieldMaps,
});
}
async deleteOne(id: string, workspaceId: string): Promise<boolean> {
async deleteOne({
deleteViewFieldInput,
workspaceId,
}: {
deleteViewFieldInput: DeleteViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMaps(workspaceId);
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const existingViewFieldToDelete = existingFlatViewFieldMaps.byId[id];
const optimisticallyUpdatedFlatViewWithDeletedAt =
fromDeleteViewFieldInputToFlatViewFieldOrThrow({
flatViewFieldMaps: existingFlatViewFieldMaps,
deleteViewFieldInput,
});
if (!isDefined(existingViewFieldToDelete)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatViewWithDeletedAt,
flatEntityMaps: existingFlatViewFieldMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
entityMaps: {
viewField: {
fromFlatViewFieldMaps: existingFlatViewFieldMaps,
toFlatViewFieldMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationOrchestratorException(
'Multiple validation errors occurred while updating view field',
);
}
const toFlatViewFieldMaps: FlatViewFieldMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: existingFlatViewFieldMaps,
entityToDeleteId: existingViewFieldToDelete.id,
const recomputedExistingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatViewWithDeletedAt.id,
flatEntityMaps: recomputedExistingFlatViewFieldMaps,
});
}
async destroyOne({
destroyViewFieldInput,
workspaceId,
}: {
destroyViewFieldInput: DestroyViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const existingFlatViewFieldMaps =
await this.getExistingFlatViewFieldMapsFromCache(workspaceId);
const existingViewFieldToDelete =
fromDestroyViewFieldInputToFlatViewFieldOrThrow({
destroyViewFieldInput,
flatViewFieldMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: existingFlatViewFieldMaps,
entityToDeleteId: existingViewFieldToDelete.id,
});
const validateAndBuildResult =
await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations(
{
@@ -239,6 +259,6 @@ export class ViewFieldV2Service {
);
}
return true;
return existingViewFieldToDelete;
}
}
@@ -110,7 +110,27 @@ export class ViewFieldService {
try {
const viewField = this.viewFieldRepository.create(viewFieldData);
return await this.viewFieldRepository.save(viewField);
const savedViewField = await this.viewFieldRepository.save(viewField);
const createdViewField = await this.findById(
savedViewField.id,
viewFieldData.workspaceId,
);
if (!isDefined(createdViewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
),
},
);
}
return createdViewField;
} catch (error) {
if (
error.message.includes('duplicate key value violates unique constraint')
@@ -175,7 +195,7 @@ export class ViewFieldService {
return viewField;
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
async destroy(id: string, workspaceId: string): Promise<ViewFieldEntity> {
const viewField = await this.findById(id, workspaceId);
if (!isDefined(viewField)) {
@@ -190,6 +210,6 @@ export class ViewFieldService {
await this.viewFieldRepository.delete(id);
return true;
return viewField;
}
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
import { ViewCacheModule } from 'src/engine/core-modules/view/cache/services/view-cache.module';
import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller';
@@ -45,6 +46,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
ViewSortEntity,
]),
I18nModule,
FeatureFlagModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
WorkspaceMigrationV2Module,