Add sync front component (#17748)
## Context Allow twenty apps to sync front components
This commit is contained in:
+1
@@ -20,6 +20,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
|
||||
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
|
||||
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
|
||||
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
|
||||
case ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND:
|
||||
throw new NotFoundError(exception);
|
||||
case ApplicationExceptionCode.FORBIDDEN:
|
||||
case ApplicationExceptionCode.INVALID_INPUT:
|
||||
|
||||
+3
-1
@@ -11,6 +11,7 @@ import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
|
||||
@@ -21,8 +22,8 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
|
||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -44,6 +45,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
FileStorageModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
FrontComponentModule,
|
||||
],
|
||||
providers: [
|
||||
ApplicationResolver,
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum ApplicationExceptionCode {
|
||||
OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND',
|
||||
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
|
||||
LOGIC_FUNCTION_NOT_FOUND = 'LOGIC_FUNCTION_NOT_FOUND',
|
||||
FRONT_COMPONENT_NOT_FOUND = 'FRONT_COMPONENT_NOT_FOUND',
|
||||
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
|
||||
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
@@ -24,6 +25,8 @@ const getApplicationExceptionUserFriendlyMessage = (
|
||||
return msg`Field not found.`;
|
||||
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
|
||||
return msg`Logic function not found.`;
|
||||
case ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND:
|
||||
return msg`Front component not found.`;
|
||||
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
|
||||
return msg`Entity not found.`;
|
||||
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
|
||||
|
||||
+109
@@ -4,6 +4,7 @@ import { parse } from 'path';
|
||||
|
||||
import {
|
||||
FieldManifest,
|
||||
FrontComponentManifest,
|
||||
LogicFunctionManifest,
|
||||
Manifest,
|
||||
ObjectFieldManifest,
|
||||
@@ -34,7 +35,9 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
|
||||
import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
|
||||
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
|
||||
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
@@ -64,6 +67,7 @@ export class ApplicationSyncService {
|
||||
private readonly fieldPermissionService: FieldPermissionService,
|
||||
private readonly permissionService: PermissionFlagService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly frontComponentService: FrontComponentService,
|
||||
) {}
|
||||
|
||||
public async synchronizeFromManifest({
|
||||
@@ -108,6 +112,14 @@ export class ApplicationSyncService {
|
||||
});
|
||||
}
|
||||
|
||||
if ((manifest.frontComponents ?? []).length > 0) {
|
||||
await this.syncFrontComponents({
|
||||
frontComponentsToSync: manifest.frontComponents,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
}
|
||||
|
||||
await this.syncRoles({
|
||||
manifest,
|
||||
workspaceId,
|
||||
@@ -948,6 +960,103 @@ export class ApplicationSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncFrontComponents({
|
||||
frontComponentsToSync,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
}: {
|
||||
frontComponentsToSync: FrontComponentManifest[];
|
||||
workspaceId: string;
|
||||
ownerFlatApplication: FlatApplication;
|
||||
}) {
|
||||
const { flatFrontComponentMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFrontComponentMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const applicationFrontComponents = Object.values(
|
||||
flatFrontComponentMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(frontComponent) =>
|
||||
isDefined(frontComponent) &&
|
||||
frontComponent.applicationId === ownerFlatApplication.id,
|
||||
) as FlatFrontComponent[];
|
||||
|
||||
const frontComponentsToSyncUniversalIdentifiers = frontComponentsToSync.map(
|
||||
(frontComponent) => frontComponent.universalIdentifier,
|
||||
);
|
||||
|
||||
const applicationFrontComponentsUniversalIdentifiers =
|
||||
applicationFrontComponents.map(
|
||||
(frontComponent) => frontComponent.universalIdentifier,
|
||||
);
|
||||
|
||||
const frontComponentsToDelete = applicationFrontComponents.filter(
|
||||
(frontComponent) =>
|
||||
isDefined(frontComponent.universalIdentifier) &&
|
||||
!frontComponentsToSyncUniversalIdentifiers.includes(
|
||||
frontComponent.universalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const frontComponentsToUpdate = applicationFrontComponents.filter(
|
||||
(frontComponent) =>
|
||||
isDefined(frontComponent.universalIdentifier) &&
|
||||
frontComponentsToSyncUniversalIdentifiers.includes(
|
||||
frontComponent.universalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const frontComponentsToCreate = frontComponentsToSync.filter(
|
||||
(frontComponentToSync) =>
|
||||
!applicationFrontComponentsUniversalIdentifiers.includes(
|
||||
frontComponentToSync.universalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
for (const frontComponentToDelete of frontComponentsToDelete) {
|
||||
await this.frontComponentService.destroyOne({
|
||||
id: frontComponentToDelete.id,
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
}
|
||||
|
||||
for (const frontComponentToUpdate of frontComponentsToUpdate) {
|
||||
const frontComponentToSync = frontComponentsToSync.find(
|
||||
(frontComponent) =>
|
||||
frontComponent.universalIdentifier ===
|
||||
frontComponentToUpdate.universalIdentifier,
|
||||
);
|
||||
|
||||
if (!frontComponentToSync) {
|
||||
throw new ApplicationException(
|
||||
`Failed to find frontComponent to sync with universalIdentifier ${frontComponentToUpdate.universalIdentifier}`,
|
||||
ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.frontComponentService.updateOne({
|
||||
id: frontComponentToUpdate.id,
|
||||
update: frontComponentToSync,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
}
|
||||
|
||||
for (const frontComponentToCreate of frontComponentsToCreate) {
|
||||
await this.frontComponentService.createOne({
|
||||
input: frontComponentToCreate,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async uninstallApplication({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
|
||||
+5
@@ -2,4 +2,9 @@ import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-
|
||||
|
||||
export const FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES = [
|
||||
'name',
|
||||
'description',
|
||||
'builtComponentChecksum',
|
||||
'sourceComponentPath',
|
||||
'builtComponentPath',
|
||||
'componentName',
|
||||
] as const satisfies (keyof FlatFrontComponent)[];
|
||||
|
||||
+9
-2
@@ -22,14 +22,21 @@ export const fromCreateFrontComponentInputToFlatFrontComponentToCreate = ({
|
||||
);
|
||||
|
||||
const id = createFrontComponentInput.id ?? v4();
|
||||
const universalIdentifier =
|
||||
createFrontComponentInput.universalIdentifier ?? v4();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
name: name ?? createFrontComponentInput.componentName,
|
||||
description: createFrontComponentInput.description ?? null,
|
||||
sourceComponentPath: createFrontComponentInput.sourceComponentPath,
|
||||
builtComponentPath: createFrontComponentInput.builtComponentPath,
|
||||
componentName: createFrontComponentInput.componentName,
|
||||
builtComponentChecksum: createFrontComponentInput.builtComponentChecksum,
|
||||
workspaceId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
universalIdentifier: id,
|
||||
universalIdentifier,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
};
|
||||
|
||||
+12
@@ -1,3 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
|
||||
|
||||
@@ -6,6 +8,16 @@ export const fromFlatFrontComponentToFrontComponentDto = (
|
||||
): FrontComponentDTO => ({
|
||||
id: flatFrontComponent.id,
|
||||
name: flatFrontComponent.name,
|
||||
description: isDefined(flatFrontComponent.description)
|
||||
? flatFrontComponent.description
|
||||
: undefined,
|
||||
sourceComponentPath: flatFrontComponent.sourceComponentPath,
|
||||
builtComponentPath: flatFrontComponent.builtComponentPath,
|
||||
componentName: flatFrontComponent.componentName,
|
||||
builtComponentChecksum: flatFrontComponent.builtComponentChecksum,
|
||||
universalIdentifier: isDefined(flatFrontComponent.universalIdentifier)
|
||||
? flatFrontComponent.universalIdentifier
|
||||
: undefined,
|
||||
workspaceId: flatFrontComponent.workspaceId,
|
||||
applicationId: flatFrontComponent.applicationId,
|
||||
createdAt: new Date(flatFrontComponent.createdAt),
|
||||
|
||||
+5
@@ -26,6 +26,11 @@ export const fromFrontComponentEntityToFlatFrontComponent = ({
|
||||
return {
|
||||
id: frontComponentEntity.id,
|
||||
name: frontComponentEntity.name,
|
||||
description: frontComponentEntity.description,
|
||||
sourceComponentPath: frontComponentEntity.sourceComponentPath,
|
||||
builtComponentPath: frontComponentEntity.builtComponentPath,
|
||||
componentName: frontComponentEntity.componentName,
|
||||
builtComponentChecksum: frontComponentEntity.builtComponentChecksum,
|
||||
workspaceId: frontComponentEntity.workspaceId,
|
||||
universalIdentifier: frontComponentEntity.universalIdentifier,
|
||||
applicationId: frontComponentEntity.applicationId,
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+8
@@ -11,6 +11,10 @@ type FrontComponentSeed = {
|
||||
workspaceId: string;
|
||||
universalIdentifier: string;
|
||||
applicationId: string;
|
||||
sourceComponentPath: string;
|
||||
builtComponentPath: string;
|
||||
componentName: string;
|
||||
builtComponentChecksum: string;
|
||||
};
|
||||
|
||||
type CommandMenuItemSeed = {
|
||||
@@ -48,6 +52,10 @@ export const getFrontComponentAndCommandMenuItemDataSeeds = (
|
||||
workspaceId,
|
||||
universalIdentifier: frontComponentId,
|
||||
applicationId,
|
||||
sourceComponentPath: 'src/front-components/demo-app.tsx',
|
||||
builtComponentPath: 'src/front-components/demo-app.mjs',
|
||||
componentName: 'DemoApp',
|
||||
builtComponentChecksum: '1234567890',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+8
@@ -28,6 +28,10 @@ export const seedFrontComponentsAndCommandMenuItems = async ({
|
||||
'workspaceId',
|
||||
'universalIdentifier',
|
||||
'applicationId',
|
||||
'sourceComponentPath',
|
||||
'builtComponentPath',
|
||||
'componentName',
|
||||
'builtComponentChecksum',
|
||||
])
|
||||
.values(
|
||||
frontComponents.map((row) => ({
|
||||
@@ -36,6 +40,10 @@ export const seedFrontComponentsAndCommandMenuItems = async ({
|
||||
workspaceId: row.workspaceId,
|
||||
universalIdentifier: row.universalIdentifier,
|
||||
applicationId: row.applicationId,
|
||||
sourceComponentPath: row.sourceComponentPath,
|
||||
builtComponentPath: row.builtComponentPath,
|
||||
componentName: row.componentName,
|
||||
builtComponentChecksum: row.builtComponentChecksum,
|
||||
})),
|
||||
)
|
||||
.orIgnore()
|
||||
|
||||
+46
-4
@@ -1,8 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import {
|
||||
FrontComponentException,
|
||||
FrontComponentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/front-component/front-component.exception';
|
||||
import { FlatCreateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
|
||||
import {
|
||||
WorkspaceMigrationActionRunnerArgs,
|
||||
@@ -14,7 +22,7 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
|
||||
'create',
|
||||
'frontComponent',
|
||||
) {
|
||||
constructor() {
|
||||
constructor(private readonly fileStorageService: FileStorageService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -27,8 +35,18 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatCreateFrontComponentAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner, workspaceId } = context;
|
||||
const { flatEntity } = flatAction;
|
||||
const { flatAction, queryRunner, workspaceId, flatApplication } = context;
|
||||
const { flatEntity: frontComponent } = flatAction;
|
||||
|
||||
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
|
||||
|
||||
if (isDefined(frontComponent.builtComponentChecksum)) {
|
||||
await this.verifySourceAndBuiltFilesExist({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
builtComponentPath: frontComponent.builtComponentPath,
|
||||
});
|
||||
}
|
||||
|
||||
const frontComponentRepository =
|
||||
queryRunner.manager.getRepository<FrontComponentEntity>(
|
||||
@@ -36,11 +54,35 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
|
||||
);
|
||||
|
||||
await frontComponentRepository.insert({
|
||||
...flatEntity,
|
||||
...frontComponent,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
private async verifySourceAndBuiltFilesExist({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
builtComponentPath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
builtComponentPath: string;
|
||||
}): Promise<void> {
|
||||
const builtExists = await this.fileStorageService.checkFileExists_v2({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
resourcePath: builtComponentPath,
|
||||
});
|
||||
|
||||
if (!builtExists) {
|
||||
throw new FrontComponentException(
|
||||
`Front component built file missing before create (built: ${builtExists})`,
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_CREATE_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatCreateFrontComponentAction>,
|
||||
): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user