Add sync front component (#17748)
## Context Allow twenty apps to sync front components
This commit is contained in:
@@ -975,8 +975,13 @@ export type CreateFieldInput = {
|
||||
};
|
||||
|
||||
export type CreateFrontComponentInput = {
|
||||
builtComponentChecksum: Scalars['String'];
|
||||
builtComponentPath: Scalars['String'];
|
||||
componentName: Scalars['String'];
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
name: Scalars['String'];
|
||||
sourceComponentPath: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateLogicFunctionInput = {
|
||||
@@ -1655,9 +1660,15 @@ export type FindAvailableSsoidpOutput = {
|
||||
export type FrontComponent = {
|
||||
__typename?: 'FrontComponent';
|
||||
applicationId: Scalars['UUID'];
|
||||
builtComponentChecksum: Scalars['String'];
|
||||
builtComponentPath: Scalars['String'];
|
||||
componentName: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
sourceComponentPath: Scalars['String'];
|
||||
universalIdentifier?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
@@ -4696,6 +4707,7 @@ export type UpdateFrontComponentInput = {
|
||||
};
|
||||
|
||||
export type UpdateFrontComponentInputUpdates = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -971,8 +971,13 @@ export type CreateFieldInput = {
|
||||
};
|
||||
|
||||
export type CreateFrontComponentInput = {
|
||||
builtComponentChecksum: Scalars['String'];
|
||||
builtComponentPath: Scalars['String'];
|
||||
componentName: Scalars['String'];
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
name: Scalars['String'];
|
||||
sourceComponentPath: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateLogicFunctionInput = {
|
||||
@@ -1627,9 +1632,15 @@ export type FindAvailableSsoidpOutput = {
|
||||
export type FrontComponent = {
|
||||
__typename?: 'FrontComponent';
|
||||
applicationId: Scalars['UUID'];
|
||||
builtComponentChecksum: Scalars['String'];
|
||||
builtComponentPath: Scalars['String'];
|
||||
componentName: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
sourceComponentPath: Scalars['String'];
|
||||
universalIdentifier?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
@@ -4535,6 +4546,7 @@ export type UpdateFrontComponentInput = {
|
||||
};
|
||||
|
||||
export type UpdateFrontComponentInputUpdates = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ type JsonManifestInput = {
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
frontComponents?: Array<{
|
||||
builtComponentChecksum?: string | null;
|
||||
builtComponentChecksum?: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
[key: string]: unknown;
|
||||
@@ -24,7 +24,7 @@ export const normalizeManifestForComparison = <T extends JsonManifestInput>(
|
||||
...component,
|
||||
builtComponentChecksum: component.builtComponentChecksum
|
||||
? '[checksum]'
|
||||
: null,
|
||||
: '',
|
||||
})),
|
||||
sources: {}, // removing sources for now, waiting compressed file implementation
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@ export const buildManifest = async (
|
||||
componentName: component.name,
|
||||
sourceComponentPath: relativeFilePath,
|
||||
builtComponentPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
|
||||
builtComponentChecksum: null,
|
||||
builtComponentChecksum: '',
|
||||
};
|
||||
|
||||
frontComponents.push(config);
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddFrontComponentColumns1770309316193
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddFrontComponentColumns1770309316193';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" ADD "description" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" ADD "sourceComponentPath" character varying NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" ADD "builtComponentPath" character varying NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" ADD "componentName" character varying NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" ADD "builtComponentChecksum" character varying NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" DROP COLUMN "builtComponentChecksum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" DROP COLUMN "componentName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" DROP COLUMN "builtComponentPath"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" DROP COLUMN "sourceComponentPath"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."frontComponent" DROP COLUMN "description"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+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> {
|
||||
|
||||
+17
-1
@@ -3,6 +3,7 @@ import { createCommandMenuItem } from 'test/integration/metadata/suites/command-
|
||||
import { deleteCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item.util';
|
||||
import { createFrontComponent } from 'test/integration/metadata/suites/front-component/utils/create-front-component.util';
|
||||
import { deleteFrontComponent } from 'test/integration/metadata/suites/front-component/utils/delete-front-component.util';
|
||||
import { seedBuiltFrontComponentFile } from 'test/integration/metadata/suites/front-component/utils/seed-built-front-component-file.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
@@ -13,6 +14,7 @@ import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/com
|
||||
describe('CommandMenuItem creation should succeed', () => {
|
||||
let createdCommandMenuItemId: string;
|
||||
let createdFrontComponentId: string | undefined;
|
||||
let cleanupBuiltFile: (() => void) | undefined;
|
||||
let companyObjectMetadataId: string;
|
||||
let personObjectMetadataId: string;
|
||||
|
||||
@@ -23,6 +25,12 @@ describe('CommandMenuItem creation should succeed', () => {
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const { cleanup } = await seedBuiltFrontComponentFile({
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
});
|
||||
|
||||
cleanupBuiltFile = cleanup;
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
@@ -52,6 +60,8 @@ describe('CommandMenuItem creation should succeed', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
cleanupBuiltFile?.();
|
||||
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
|
||||
value: false,
|
||||
@@ -176,7 +186,13 @@ describe('CommandMenuItem creation should succeed', () => {
|
||||
it('should create command menu item with frontComponentId', async () => {
|
||||
const { data: frontComponentData } = await createFrontComponent({
|
||||
expectToFail: false,
|
||||
input: { name: 'Test Front Component' },
|
||||
input: {
|
||||
name: 'Test Front Component',
|
||||
componentName: 'TestFrontComponent',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
});
|
||||
|
||||
createdFrontComponentId = frontComponentData?.createFrontComponent?.id;
|
||||
|
||||
-14
@@ -37,20 +37,6 @@ exports[`Front component creation should fail when name is empty 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when name is too long 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Expected non-nullable type "String!" not to be null.",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when name is whitespace-only 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ exports[`Front component deletion should fail when front component does not exis
|
||||
"subCode": "FRONT_COMPONENT_NOT_FOUND",
|
||||
"userFriendlyMessage": "Front component not found.",
|
||||
},
|
||||
"message": "Front component not found",
|
||||
"message": "Front component to destroy not found",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
+31
-12
@@ -1,5 +1,6 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createFrontComponent } from 'test/integration/metadata/suites/front-component/utils/create-front-component.util';
|
||||
import { seedBuiltFrontComponentFile } from 'test/integration/metadata/suites/front-component/utils/seed-built-front-component-file.util';
|
||||
import {
|
||||
type EachTestingContext,
|
||||
eachTestingContextFilter,
|
||||
@@ -8,39 +9,57 @@ import {
|
||||
import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
|
||||
|
||||
type TestContext = {
|
||||
name: string | null;
|
||||
input: CreateFrontComponentInput;
|
||||
};
|
||||
|
||||
const FAILING_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
{
|
||||
title: 'when name is empty',
|
||||
context: {
|
||||
name: '',
|
||||
input: {
|
||||
name: '',
|
||||
componentName: 'TestComponent',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when name is whitespace-only',
|
||||
context: {
|
||||
name: ' ',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when name is too long',
|
||||
context: {
|
||||
name: null,
|
||||
input: {
|
||||
name: ' ',
|
||||
componentName: 'TestComponent',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Front component creation should fail', () => {
|
||||
let cleanupBuiltFile: (() => void) | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { cleanup } = await seedBuiltFrontComponentFile({
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
});
|
||||
|
||||
cleanupBuiltFile = cleanup;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupBuiltFile?.();
|
||||
});
|
||||
|
||||
it.each(eachTestingContextFilter(FAILING_TEST_CASES))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const { errors } = await createFrontComponent({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
name: context.name,
|
||||
} as CreateFrontComponentInput,
|
||||
input: context.input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
|
||||
+22
@@ -1,8 +1,22 @@
|
||||
import { createFrontComponent } from 'test/integration/metadata/suites/front-component/utils/create-front-component.util';
|
||||
import { deleteFrontComponent } from 'test/integration/metadata/suites/front-component/utils/delete-front-component.util';
|
||||
import { seedBuiltFrontComponentFile } from 'test/integration/metadata/suites/front-component/utils/seed-built-front-component-file.util';
|
||||
|
||||
describe('Front component creation should succeed', () => {
|
||||
let createdFrontComponentId: string | undefined;
|
||||
let cleanupBuiltFile: (() => void) | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { cleanup } = await seedBuiltFrontComponentFile({
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
});
|
||||
|
||||
cleanupBuiltFile = cleanup;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupBuiltFile?.();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdFrontComponentId) {
|
||||
@@ -19,6 +33,10 @@ describe('Front component creation should succeed', () => {
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'testFrontComponent',
|
||||
componentName: 'TestFrontComponent',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -35,6 +53,10 @@ describe('Front component creation should succeed', () => {
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: ' frontComponentWithSpaces ',
|
||||
componentName: 'FrontComponentWithSpaces',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+19
@@ -1,13 +1,32 @@
|
||||
import { createFrontComponent } from 'test/integration/metadata/suites/front-component/utils/create-front-component.util';
|
||||
import { deleteFrontComponent } from 'test/integration/metadata/suites/front-component/utils/delete-front-component.util';
|
||||
import { findFrontComponent } from 'test/integration/metadata/suites/front-component/utils/find-front-component.util';
|
||||
import { seedBuiltFrontComponentFile } from 'test/integration/metadata/suites/front-component/utils/seed-built-front-component-file.util';
|
||||
|
||||
describe('Front component deletion should succeed', () => {
|
||||
let cleanupBuiltFile: (() => void) | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { cleanup } = await seedBuiltFrontComponentFile({
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
});
|
||||
|
||||
cleanupBuiltFile = cleanup;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupBuiltFile?.();
|
||||
});
|
||||
|
||||
it('should successfully delete a front component', async () => {
|
||||
const { data: createData } = await createFrontComponent({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'frontComponentToDelete',
|
||||
componentName: 'FrontComponentToDelete',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+18
@@ -1,16 +1,34 @@
|
||||
import { createFrontComponent } from 'test/integration/metadata/suites/front-component/utils/create-front-component.util';
|
||||
import { deleteFrontComponent } from 'test/integration/metadata/suites/front-component/utils/delete-front-component.util';
|
||||
import { seedBuiltFrontComponentFile } from 'test/integration/metadata/suites/front-component/utils/seed-built-front-component-file.util';
|
||||
import { updateFrontComponent } from 'test/integration/metadata/suites/front-component/utils/update-front-component.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
describe('Front component update should succeed', () => {
|
||||
let testFrontComponentId: string | undefined;
|
||||
let cleanupBuiltFile: (() => void) | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { cleanup } = await seedBuiltFrontComponentFile({
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
});
|
||||
|
||||
cleanupBuiltFile = cleanup;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupBuiltFile?.();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createFrontComponent({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'testFrontComponentToUpdate',
|
||||
componentName: 'TestFrontComponentToUpdate',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const STORAGE_LOCAL_PATH = join(process.cwd(), '.local-storage');
|
||||
|
||||
const getWorkspaceCustomApplicationUniversalIdentifier = async (
|
||||
workspaceId: string,
|
||||
): Promise<string> => {
|
||||
const result = await global.testDataSource.query(
|
||||
'SELECT "workspaceCustomApplicationId" FROM core.workspace WHERE id = $1',
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
return result[0].workspaceCustomApplicationId;
|
||||
};
|
||||
|
||||
export const seedBuiltFrontComponentFile = async ({
|
||||
workspaceId = SEED_APPLE_WORKSPACE_ID,
|
||||
builtComponentPath,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
builtComponentPath: string;
|
||||
}): Promise<{ cleanup: () => void }> => {
|
||||
const applicationUniversalIdentifier =
|
||||
await getWorkspaceCustomApplicationUniversalIdentifier(workspaceId);
|
||||
|
||||
const filePath = join(
|
||||
STORAGE_LOCAL_PATH,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
'built-front-component',
|
||||
builtComponentPath,
|
||||
);
|
||||
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, 'dummy built component content');
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
if (existsSync(filePath)) {
|
||||
rmSync(filePath);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,6 @@ export type FrontComponentManifest = {
|
||||
description?: string;
|
||||
sourceComponentPath: string;
|
||||
builtComponentPath: string;
|
||||
builtComponentChecksum: string | null;
|
||||
builtComponentChecksum: string;
|
||||
componentName: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user