Add sync front component (#17748)
## Context Allow twenty apps to sync front components
This commit is contained in:
+31
-2
@@ -1,4 +1,4 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
@@ -11,8 +11,37 @@ export class CreateFrontComponentInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@HideField()
|
||||
universalIdentifier?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
sourceComponentPath: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
builtComponentPath: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
componentName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
builtComponentChecksum: string;
|
||||
}
|
||||
|
||||
+34
-1
@@ -1,6 +1,12 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsDateString, IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
import {
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@@ -15,6 +21,33 @@ export class FrontComponentDTO {
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
sourceComponentPath: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
builtComponentPath: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
componentName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
builtComponentChecksum: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
universalIdentifier?: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
|
||||
+11
-1
@@ -1,4 +1,4 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
@@ -17,6 +17,16 @@ export class UpdateFrontComponentInputUpdates {
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@HideField()
|
||||
builtComponentChecksum?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
+19
-1
@@ -9,13 +9,31 @@ import {
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity('frontComponent')
|
||||
export class FrontComponentEntity extends SyncableEntity {
|
||||
export class FrontComponentEntity
|
||||
extends SyncableEntity
|
||||
implements Required<FrontComponentEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
description: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
sourceComponentPath: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
builtComponentPath: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
componentName: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
builtComponentChecksum: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+6
@@ -8,6 +8,8 @@ export enum FrontComponentExceptionCode {
|
||||
FRONT_COMPONENT_NOT_FOUND = 'FRONT_COMPONENT_NOT_FOUND',
|
||||
FRONT_COMPONENT_ALREADY_EXISTS = 'FRONT_COMPONENT_ALREADY_EXISTS',
|
||||
INVALID_FRONT_COMPONENT_INPUT = 'INVALID_FRONT_COMPONENT_INPUT',
|
||||
FRONT_COMPONENT_CREATE_FAILED = 'FRONT_COMPONENT_CREATE_FAILED',
|
||||
FRONT_COMPONENT_NOT_READY = 'FRONT_COMPONENT_NOT_READY',
|
||||
}
|
||||
|
||||
const getFrontComponentExceptionUserFriendlyMessage = (
|
||||
@@ -20,6 +22,10 @@ const getFrontComponentExceptionUserFriendlyMessage = (
|
||||
return msg`A front component with this name already exists.`;
|
||||
case FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT:
|
||||
return msg`Invalid front component input.`;
|
||||
case FrontComponentExceptionCode.FRONT_COMPONENT_CREATE_FAILED:
|
||||
return msg`Failed to create front component.`;
|
||||
case FrontComponentExceptionCode.FRONT_COMPONENT_NOT_READY:
|
||||
return msg`Front component is not ready.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+20
-3
@@ -9,6 +9,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { fromFlatFrontComponentToFrontComponentDto } from 'src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util';
|
||||
import { CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
|
||||
import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
|
||||
import { UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
|
||||
@@ -48,7 +49,12 @@ export class FrontComponentResolver {
|
||||
@Args('input') input: CreateFrontComponentInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<FrontComponentDTO> {
|
||||
return await this.frontComponentService.create(input, workspace.id);
|
||||
const flatFrontComponent = await this.frontComponentService.createOne({
|
||||
input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent);
|
||||
}
|
||||
|
||||
@Mutation(() => FrontComponentDTO)
|
||||
@@ -57,7 +63,13 @@ export class FrontComponentResolver {
|
||||
@Args('input') input: UpdateFrontComponentInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<FrontComponentDTO> {
|
||||
return await this.frontComponentService.update(input, workspace.id);
|
||||
const flatFrontComponent = await this.frontComponentService.updateOne({
|
||||
id: input.id,
|
||||
update: input.update,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent);
|
||||
}
|
||||
|
||||
@Mutation(() => FrontComponentDTO)
|
||||
@@ -66,6 +78,11 @@ export class FrontComponentResolver {
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<FrontComponentDTO> {
|
||||
return await this.frontComponentService.delete(id, workspace.id);
|
||||
const flatFrontComponent = await this.frontComponentService.destroyOne({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent);
|
||||
}
|
||||
}
|
||||
|
||||
+82
-50
@@ -3,11 +3,12 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { fromCreateFrontComponentInputToFlatFrontComponentToCreate } from 'src/engine/metadata-modules/flat-front-component/utils/from-create-front-component-input-to-flat-front-component-to-create.util';
|
||||
import { fromDeleteFrontComponentInputToFlatFrontComponentOrThrow } from 'src/engine/metadata-modules/flat-front-component/utils/from-delete-front-component-input-to-flat-front-component-or-throw.util';
|
||||
import { fromFlatFrontComponentToFrontComponentDto } from 'src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util';
|
||||
import { fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow } from 'src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util';
|
||||
import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
|
||||
@@ -67,20 +68,28 @@ export class FrontComponentService {
|
||||
return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent);
|
||||
}
|
||||
|
||||
async create(
|
||||
input: CreateFrontComponentInput,
|
||||
workspaceId: string,
|
||||
): Promise<FrontComponentDTO> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
async createOne({
|
||||
input,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
}: {
|
||||
input: Omit<CreateFrontComponentInput, 'applicationId'>;
|
||||
workspaceId: string;
|
||||
ownerFlatApplication?: FlatApplication;
|
||||
}): Promise<FlatFrontComponent> {
|
||||
const resolvedOwnerFlatApplication =
|
||||
ownerFlatApplication ??
|
||||
(
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
)
|
||||
).workspaceCustomFlatApplication;
|
||||
|
||||
const flatFrontComponentToCreate =
|
||||
fromCreateFrontComponentInputToFlatFrontComponentToCreate({
|
||||
createFrontComponentInput: input,
|
||||
workspaceId,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
@@ -96,7 +105,7 @@ export class FrontComponentService {
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
resolvedOwnerFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -115,22 +124,30 @@ export class FrontComponentService {
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatFrontComponentToFrontComponentDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatFrontComponentToCreate.id,
|
||||
flatEntityMaps: recomputedFlatFrontComponentMaps,
|
||||
}),
|
||||
);
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatFrontComponentToCreate.id,
|
||||
flatEntityMaps: recomputedFlatFrontComponentMaps,
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
input: UpdateFrontComponentInput,
|
||||
workspaceId: string,
|
||||
): Promise<FrontComponentDTO> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
async updateOne({
|
||||
id,
|
||||
update,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
}: {
|
||||
id: string;
|
||||
update: UpdateFrontComponentInput['update'];
|
||||
workspaceId: string;
|
||||
ownerFlatApplication?: FlatApplication;
|
||||
}): Promise<FlatFrontComponent> {
|
||||
const resolvedOwnerFlatApplication =
|
||||
ownerFlatApplication ??
|
||||
(
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
)
|
||||
).workspaceCustomFlatApplication;
|
||||
|
||||
const { flatFrontComponentMaps: existingFlatFrontComponentMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
@@ -143,7 +160,7 @@ export class FrontComponentService {
|
||||
const flatFrontComponentToUpdate =
|
||||
fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow({
|
||||
flatFrontComponentMaps: existingFlatFrontComponentMaps,
|
||||
updateFrontComponentInput: input,
|
||||
updateFrontComponentInput: { id, update },
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
@@ -159,7 +176,7 @@ export class FrontComponentService {
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
resolvedOwnerFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -178,19 +195,30 @@ export class FrontComponentService {
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatFrontComponentToFrontComponentDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: input.id,
|
||||
flatEntityMaps: recomputedFlatFrontComponentMaps,
|
||||
}),
|
||||
);
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatFrontComponentMaps,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<FrontComponentDTO> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
async destroyOne({
|
||||
id,
|
||||
workspaceId,
|
||||
isSystemBuild = false,
|
||||
ownerFlatApplication,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
isSystemBuild?: boolean;
|
||||
ownerFlatApplication?: FlatApplication;
|
||||
}): Promise<FlatFrontComponent> {
|
||||
const resolvedOwnerFlatApplication =
|
||||
ownerFlatApplication ??
|
||||
(
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
)
|
||||
).workspaceCustomFlatApplication;
|
||||
|
||||
const { flatFrontComponentMaps: existingFlatFrontComponentMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
@@ -200,11 +228,17 @@ export class FrontComponentService {
|
||||
},
|
||||
);
|
||||
|
||||
const flatFrontComponentToDelete =
|
||||
fromDeleteFrontComponentInputToFlatFrontComponentOrThrow({
|
||||
flatFrontComponentMaps: existingFlatFrontComponentMaps,
|
||||
frontComponentId: id,
|
||||
});
|
||||
const existingFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: existingFlatFrontComponentMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatFrontComponent)) {
|
||||
throw new FrontComponentException(
|
||||
'Front component to destroy not found',
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
@@ -212,27 +246,25 @@ export class FrontComponentService {
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
frontComponent: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatFrontComponentToDelete],
|
||||
flatEntityToDelete: [existingFlatFrontComponent],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
isSystemBuild,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
resolvedOwnerFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting front component',
|
||||
'Multiple validation errors occurred while destroying front component',
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatFrontComponentToFrontComponentDto(
|
||||
flatFrontComponentToDelete,
|
||||
);
|
||||
return existingFlatFrontComponent;
|
||||
}
|
||||
|
||||
async findByIdOrThrow(
|
||||
|
||||
+5
@@ -2,6 +2,7 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
@@ -19,6 +20,10 @@ export const frontComponentGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new UserInputError(error);
|
||||
case FrontComponentExceptionCode.FRONT_COMPONENT_ALREADY_EXISTS:
|
||||
throw new ConflictError(error);
|
||||
case FrontComponentExceptionCode.FRONT_COMPONENT_NOT_READY:
|
||||
throw new ForbiddenError(error);
|
||||
case FrontComponentExceptionCode.FRONT_COMPONENT_CREATE_FAILED:
|
||||
throw error;
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user