diff --git a/.vscode/launch.json b/.vscode/launch.json index 01396f38eb6..9ff10b3883a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -73,7 +73,8 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "env": { - "NODE_ENV": "test" + "NODE_ENV": "test", + "NODE_OPTIONS": "--max-old-space-size=12288 --import tsx/esm" } }, { diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 236a51bbfd0..a65d5252f7f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -875,6 +875,11 @@ export type CreateFieldInput = { type: FieldMetadataType; }; +export type CreateFrontComponentInput = { + id?: InputMaybe; + name: Scalars['String']; +}; + export type CreateObjectInput = { description?: InputMaybe; icon?: InputMaybe; @@ -986,6 +991,7 @@ export type CreateSkillInput = { content: Scalars['String']; description?: InputMaybe; icon?: InputMaybe; + id?: InputMaybe; label: Scalars['String']; name: Scalars['String']; }; @@ -1533,6 +1539,15 @@ export type FindAvailableSsoidpOutput = { workspace: WorkspaceNameAndId; }; +export type FrontComponent = { + __typename?: 'FrontComponent'; + applicationId: Scalars['UUID']; + createdAt: Scalars['DateTime']; + id: Scalars['UUID']; + name: Scalars['String']; + updatedAt: Scalars['DateTime']; +}; + export type FullName = { __typename?: 'FullName'; firstName: Scalars['String']; @@ -1873,6 +1888,7 @@ export type Mutation = { createDraftFromWorkflowVersion: WorkflowVersionDto; createEmailingDomain: EmailingDomain; createFile: File; + createFrontComponent: FrontComponent; createManyCoreViewFields: Array; createManyCoreViewGroups: Array; createOIDCIdentityProvider: SetupSsoOutput; @@ -1911,6 +1927,7 @@ export type Mutation = { deleteDatabaseConfigVariable: Scalars['Boolean']; deleteEmailingDomain: Scalars['Boolean']; deleteFile: File; + deleteFrontComponent: FrontComponent; deleteJobs: DeleteJobsResponse; deleteOneAgent: Agent; deleteOneCronTrigger: CronTrigger; @@ -1997,6 +2014,7 @@ export type Mutation = { updateCoreViewGroup: CoreViewGroup; updateCoreViewSort: CoreViewSort; updateDatabaseConfigVariable: Scalars['Boolean']; + updateFrontComponent: FrontComponent; updateLabPublicFeatureFlag: FeatureFlagDto; updateOneAgent: Agent; updateOneApplicationVariable: Scalars['Boolean']; @@ -2159,6 +2177,11 @@ export type MutationCreateFileArgs = { }; +export type MutationCreateFrontComponentArgs = { + input: CreateFrontComponentInput; +}; + + export type MutationCreateManyCoreViewFieldsArgs = { inputs: Array; }; @@ -2348,6 +2371,11 @@ export type MutationDeleteFileArgs = { }; +export type MutationDeleteFrontComponentArgs = { + id: Scalars['UUID']; +}; + + export type MutationDeleteJobsArgs = { jobIds: Array; queueName: Scalars['String']; @@ -2767,6 +2795,11 @@ export type MutationUpdateDatabaseConfigVariableArgs = { }; +export type MutationUpdateFrontComponentArgs = { + input: UpdateFrontComponentInput; +}; + + export type MutationUpdateLabPublicFeatureFlagArgs = { input: UpdateLabPublicFeatureFlagInput; }; @@ -3377,6 +3410,8 @@ export type Query = { findOneServerlessFunction: ServerlessFunction; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; + frontComponent?: Maybe; + frontComponents: Array; getAddressDetails: PlaceDetailsResult; getApprovedAccessDomains: Array; getAutoCompleteAddress: Array; @@ -3520,6 +3555,11 @@ export type QueryFindWorkspaceFromInviteHashArgs = { }; +export type QueryFrontComponentArgs = { + id: Scalars['UUID']; +}; + + export type QueryGetAddressDetailsArgs = { placeId: Scalars['String']; token: Scalars['String']; @@ -4493,6 +4533,17 @@ export type UpdateFieldInput = { settings?: InputMaybe; }; +export type UpdateFrontComponentInput = { + /** The id of the front component to update */ + id: Scalars['UUID']; + /** The front component fields to update */ + update: UpdateFrontComponentInputUpdates; +}; + +export type UpdateFrontComponentInputUpdates = { + name?: InputMaybe; +}; + export type UpdateLabPublicFeatureFlagInput = { publicFeatureFlag: Scalars['String']; value: Scalars['Boolean']; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 0ddca5f1603..a71e0540469 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1516,6 +1516,15 @@ export type FindAvailableSsoidpOutput = { workspace: WorkspaceNameAndId; }; +export type FrontComponent = { + __typename?: 'FrontComponent'; + applicationId: Scalars['UUID']; + createdAt: Scalars['DateTime']; + id: Scalars['UUID']; + name: Scalars['String']; + updatedAt: Scalars['DateTime']; +}; + export type FullName = { __typename?: 'FullName'; firstName: Scalars['String']; diff --git a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts index 596aa8953f9..e5e3cac0f4b 100644 --- a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts +++ b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts @@ -42,6 +42,7 @@ export const useMetadataErrorHandler = () => { rowLevelPermissionPredicate: t`row level permission predicate`, rowLevelPermissionPredicateGroup: t`row level permission predicate group`, viewFilterGroup: t`view filter group`, + frontComponent: t`front component`, } as const satisfies Record; const handleMetadataError = ( diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1768495429374-addFrontComponent.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1768495429374-addFrontComponent.ts new file mode 100644 index 00000000000..faf584cfe83 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1768495429374-addFrontComponent.ts @@ -0,0 +1,33 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddFrontComponent1768495429374 implements MigrationInterface { + name = 'AddFrontComponent1768495429374'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."frontComponent" ("workspaceId" uuid NOT NULL, "universalIdentifier" uuid NOT NULL, "applicationId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_843479d93ef40e58dc4587339aa" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_a1413f7f0e71cb5825ac40c4fa" ON "core"."frontComponent" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."frontComponent" ADD CONSTRAINT "FK_b5e4eea33659f066e865ab6afe0" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."frontComponent" ADD CONSTRAINT "FK_63e430d5f8e554c4282e7b48876" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."frontComponent" DROP CONSTRAINT "FK_63e430d5f8e554c4282e7b48876"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."frontComponent" DROP CONSTRAINT "FK_b5e4eea33659f066e865ab6afe0"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_a1413f7f0e71cb5825ac40c4fa"`, + ); + await queryRunner.query(`DROP TABLE "core"."frontComponent"`); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index 82595066306..1fd36d5070f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -5,6 +5,7 @@ import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/meta import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; +import { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant'; import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant'; import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant'; import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant'; @@ -162,6 +163,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { ], propertiesToStringify: [], }, + frontComponent: { + propertiesToCompare: [...FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES], + propertiesToStringify: [], + }, } as const satisfies { [P in AllMetadataName]: OneFlatEntityConfiguration

; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts index f9ed0f0b1c6..3f9067d7542 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts @@ -436,6 +436,13 @@ export const ALL_METADATA_RELATIONS = { }, }, }, + frontComponent: { + manyToOne: { + workspace: null, + application: null, + }, + oneToMany: {}, + }, } as const satisfies MetadataRelationsProperties; // Note: satisfies with complex mapped types involving nested generics doesn't always catch missing required keys diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts index dc2c1cd6487..455a6a99e9a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts @@ -82,4 +82,5 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { role: true, objectMetadata: true, }, + frontComponent: {}, } as const satisfies MetadataRequiredForValidation; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts index 3d2711d4c6f..3db62244a2f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts @@ -1,4 +1,6 @@ import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; +import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; +import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; @@ -145,6 +147,11 @@ import { type DeleteViewAction, type UpdateViewAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type'; +import { + type CreateFrontComponentAction, + type DeleteFrontComponentAction, + type UpdateFrontComponentAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type'; export type AllFlatEntityTypesByMetadataName = { fieldMetadata: { @@ -336,4 +343,13 @@ export type AllFlatEntityTypesByMetadataName = { flatEntity: FlatPageLayoutTab; entity: PageLayoutTabEntity; }; + frontComponent: { + actions: { + create: CreateFrontComponentAction; + update: UpdateFrontComponentAction; + delete: DeleteFrontComponentAction; + }; + flatEntity: FlatFrontComponent; + entity: FrontComponentEntity; + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap index 5474ef65f54..a6c50fc2b5a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`; @@ -24,6 +24,8 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo ] `; +exports[`getMetadataRelatedMetadataNames should return related metadata names for frontComponent 1`] = `[]`; + exports[`getMetadataRelatedMetadataNames should return related metadata names for index 1`] = ` [ "objectMetadata", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant.ts new file mode 100644 index 00000000000..b3f904377d0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant.ts @@ -0,0 +1,5 @@ +import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; + +export const FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES = [ + 'name', +] as const satisfies (keyof FlatFrontComponent)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts new file mode 100644 index 00000000000..aeaacf17c3b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { WorkspaceFlatFrontComponentMapCacheService } from 'src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service'; +import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([FrontComponentEntity]), + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], + providers: [WorkspaceFlatFrontComponentMapCacheService], + exports: [WorkspaceFlatFrontComponentMapCacheService], +}) +export class FlatFrontComponentModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts new file mode 100644 index 00000000000..1c64005e601 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatFrontComponentMaps } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type'; +import { fromFrontComponentEntityToFlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util'; +import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; + +@Injectable() +@WorkspaceCache('flatFrontComponentMaps') +export class WorkspaceFlatFrontComponentMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(FrontComponentEntity) + private readonly frontComponentRepository: Repository, + ) { + super(); + } + + async computeForCache(workspaceId: string): Promise { + const frontComponents = await this.frontComponentRepository.find({ + where: { workspaceId }, + }); + + const flatFrontComponentMaps = createEmptyFlatEntityMaps(); + + for (const frontComponentEntity of frontComponents) { + const flatFrontComponent = + fromFrontComponentEntityToFlatFrontComponent(frontComponentEntity); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatFrontComponent, + flatEntityMapsToMutate: flatFrontComponentMaps, + }); + } + + return flatFrontComponentMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type.ts new file mode 100644 index 00000000000..6a5ac1daa27 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; + +export type FlatFrontComponentMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/types/flat-front-component.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/types/flat-front-component.type.ts new file mode 100644 index 00000000000..a8907b346d5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/types/flat-front-component.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; +import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; + +export type FlatFrontComponent = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-create-front-component-input-to-flat-front-component-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-create-front-component-input-to-flat-front-component-to-create.util.ts new file mode 100644 index 00000000000..2d7ec5e9639 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-create-front-component-input-to-flat-front-component-to-create.util.ts @@ -0,0 +1,34 @@ +import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; +import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input'; + +export const fromCreateFrontComponentInputToFlatFrontComponentToCreate = ({ + createFrontComponentInput, + workspaceId, + applicationId, +}: { + createFrontComponentInput: CreateFrontComponentInput; + workspaceId: string; + applicationId: string; +}): FlatFrontComponent => { + const now = new Date().toISOString(); + + const { name } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + createFrontComponentInput, + ['name'], + ); + + const id = createFrontComponentInput.id ?? v4(); + + return { + id, + name, + workspaceId, + createdAt: now, + updatedAt: now, + universalIdentifier: id, + applicationId, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-delete-front-component-input-to-flat-front-component-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-delete-front-component-input-to-flat-front-component-or-throw.util.ts new file mode 100644 index 00000000000..974bd890377 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-delete-front-component-input-to-flat-front-component-or-throw.util.ts @@ -0,0 +1,31 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +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 { + FrontComponentException, + FrontComponentExceptionCode, +} from 'src/engine/metadata-modules/front-component/front-component.exception'; + +export const fromDeleteFrontComponentInputToFlatFrontComponentOrThrow = ({ + flatFrontComponentMaps, + frontComponentId, +}: { + flatFrontComponentMaps: FlatEntityMaps; + frontComponentId: string; +}): FlatFrontComponent => { + const existingFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: frontComponentId, + flatEntityMaps: flatFrontComponentMaps, + }); + + if (!isDefined(existingFlatFrontComponent)) { + throw new FrontComponentException( + 'Front component not found', + FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, + ); + } + + return existingFlatFrontComponent; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util.ts new file mode 100644 index 00000000000..7a66631c63d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util.ts @@ -0,0 +1,13 @@ +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'; + +export const fromFlatFrontComponentToFrontComponentDto = ( + flatFrontComponent: FlatFrontComponent, +): FrontComponentDTO => ({ + id: flatFrontComponent.id, + name: flatFrontComponent.name, + workspaceId: flatFrontComponent.workspaceId, + applicationId: flatFrontComponent.applicationId, + createdAt: new Date(flatFrontComponent.createdAt), + updatedAt: new Date(flatFrontComponent.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts new file mode 100644 index 00000000000..bb413e054be --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts @@ -0,0 +1,16 @@ +import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; +import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; + +export const fromFrontComponentEntityToFlatFrontComponent = ( + frontComponentEntity: FrontComponentEntity, +): FlatFrontComponent => { + return { + id: frontComponentEntity.id, + name: frontComponentEntity.name, + workspaceId: frontComponentEntity.workspaceId, + universalIdentifier: frontComponentEntity.universalIdentifier, + applicationId: frontComponentEntity.applicationId, + createdAt: frontComponentEntity.createdAt.toISOString(), + updatedAt: frontComponentEntity.updatedAt.toISOString(), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util.ts new file mode 100644 index 00000000000..a3bd6882a34 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util.ts @@ -0,0 +1,42 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant'; +import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; +import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input'; +import { + FrontComponentException, + FrontComponentExceptionCode, +} from 'src/engine/metadata-modules/front-component/front-component.exception'; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; + +export const fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow = + ({ + flatFrontComponentMaps, + updateFrontComponentInput, + }: { + flatFrontComponentMaps: FlatEntityMaps; + updateFrontComponentInput: UpdateFrontComponentInput; + }): FlatFrontComponent => { + const existingFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: updateFrontComponentInput.id, + flatEntityMaps: flatFrontComponentMaps, + }); + + if (!isDefined(existingFlatFrontComponent)) { + throw new FrontComponentException( + 'Front component not found', + FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, + ); + } + + return { + ...mergeUpdateInExistingRecord({ + existing: existingFlatFrontComponent, + properties: [...FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES], + update: updateFrontComponentInput.update, + }), + updatedAt: new Date().toISOString(), + }; + }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-create-skill-input-to-flat-skill-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-create-skill-input-to-flat-skill-to-create.util.ts index 86837e30ed9..57d799551b3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-create-skill-input-to-flat-skill-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-create-skill-input-to-flat-skill-to-create.util.ts @@ -24,7 +24,7 @@ export const fromCreateSkillInputToFlatSkillToCreate = ({ // Content is markdown - only trim, don't collapse whitespace (preserve newlines) const content = createSkillInput.content.trim(); - const id = v4(); + const id = createSkillInput.id ?? v4(); return { id, diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/create-front-component.input.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/create-front-component.input.ts new file mode 100644 index 00000000000..a5f2961bff5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/create-front-component.input.ts @@ -0,0 +1,18 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class CreateFrontComponentInput { + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + + @IsString() + @IsNotEmpty() + @Field() + name: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts new file mode 100644 index 00000000000..f773b1a2653 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts @@ -0,0 +1,31 @@ +import { Field, HideField, ObjectType } from '@nestjs/graphql'; + +import { IsDateString, IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('FrontComponent') +export class FrontComponentDTO { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @IsString() + @Field() + name: string; + + @HideField() + workspaceId: string; + + @Field(() => UUIDScalarType) + applicationId: string; + + @IsDateString() + @Field() + createdAt: Date; + + @IsDateString() + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/update-front-component.input.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/update-front-component.input.ts new file mode 100644 index 00000000000..561b8eb41a5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/update-front-component.input.ts @@ -0,0 +1,37 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class UpdateFrontComponentInputUpdates { + @IsOptional() + @IsString() + @Field({ nullable: true }) + name?: string; +} + +@InputType() +export class UpdateFrontComponentInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType, { + description: 'The id of the front component to update', + }) + id: string; + + @Type(() => UpdateFrontComponentInputUpdates) + @ValidateNested() + @Field(() => UpdateFrontComponentInputUpdates, { + description: 'The front component fields to update', + }) + update: UpdateFrontComponentInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/entities/front-component.entity.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/entities/front-component.entity.ts new file mode 100644 index 00000000000..c8f18f559b3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/entities/front-component.entity.ts @@ -0,0 +1,27 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/syncable-entity-required.interface'; + +@Entity('frontComponent') +export class FrontComponentEntity + extends SyncableEntityRequired + implements Required +{ + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false }) + name: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.exception.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.exception.ts new file mode 100644 index 00000000000..15c52c161f0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.exception.ts @@ -0,0 +1,40 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { CustomException } from 'src/utils/custom-exception'; + +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', +} + +const getFrontComponentExceptionUserFriendlyMessage = ( + code: FrontComponentExceptionCode, +) => { + switch (code) { + case FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND: + return msg`Front component not found.`; + case FrontComponentExceptionCode.FRONT_COMPONENT_ALREADY_EXISTS: + return msg`A front component with this name already exists.`; + case FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT: + return msg`Invalid front component input.`; + default: + assertUnreachable(code); + } +}; + +export class FrontComponentException extends CustomException { + constructor( + message: string, + code: FrontComponentExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? + getFrontComponentExceptionUserFriendlyMessage(code), + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts new file mode 100644 index 00000000000..cd4d9b38061 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; + +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { FlatFrontComponentModule } from 'src/engine/metadata-modules/flat-front-component/flat-front-component.module'; +import { FrontComponentResolver } from 'src/engine/metadata-modules/front-component/front-component.resolver'; +import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service'; +import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; + +@Module({ + imports: [ + WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceMigrationModule, + ApplicationModule, + PermissionsModule, + FlatFrontComponentModule, + ], + providers: [ + FrontComponentService, + FrontComponentResolver, + FrontComponentGraphqlApiExceptionInterceptor, + WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + ], + exports: [FrontComponentService], +}) +export class FrontComponentModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts new file mode 100644 index 00000000000..a17006880cc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts @@ -0,0 +1,71 @@ +import { UseGuards, UseInterceptors } from '@nestjs/common'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; + +import { PermissionFlagType } from 'twenty-shared/constants'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +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 { 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'; +import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service'; +import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor'; +import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; + +@UseGuards(WorkspaceAuthGuard) +@UseInterceptors( + WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + FrontComponentGraphqlApiExceptionInterceptor, +) +@Resolver(() => FrontComponentDTO) +export class FrontComponentResolver { + constructor(private readonly frontComponentService: FrontComponentService) {} + + @Query(() => [FrontComponentDTO]) + @UseGuards(NoPermissionGuard) + async frontComponents( + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return await this.frontComponentService.findAll(workspace.id); + } + + @Query(() => FrontComponentDTO, { nullable: true }) + @UseGuards(NoPermissionGuard) + async frontComponent( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return await this.frontComponentService.findById(id, workspace.id); + } + + @Mutation(() => FrontComponentDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS)) + async createFrontComponent( + @Args('input') input: CreateFrontComponentInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return await this.frontComponentService.create(input, workspace.id); + } + + @Mutation(() => FrontComponentDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS)) + async updateFrontComponent( + @Args('input') input: UpdateFrontComponentInput, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return await this.frontComponentService.update(input, workspace.id); + } + + @Mutation(() => FrontComponentDTO) + @UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS)) + async deleteFrontComponent( + @Args('id', { type: () => UUIDScalarType }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return await this.frontComponentService.delete(id, workspace.id); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts new file mode 100644 index 00000000000..be34be565ae --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.service.ts @@ -0,0 +1,237 @@ +import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'twenty-shared/utils'; + +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +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 { 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'; +import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; +import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input'; +import { + FrontComponentException, + FrontComponentExceptionCode, +} from 'src/engine/metadata-modules/front-component/front-component.exception'; +import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; + +@Injectable() +export class FrontComponentService { + constructor( + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly applicationService: ApplicationService, + ) {} + + async findAll(workspaceId: string): Promise { + const { flatFrontComponentMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFrontComponentMaps'], + }, + ); + + return Object.values(flatFrontComponentMaps.byId) + .filter(isDefined) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(fromFlatFrontComponentToFrontComponentDto); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + const { flatFrontComponentMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFrontComponentMaps'], + }, + ); + + const flatFrontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatFrontComponentMaps, + }); + + if (!isDefined(flatFrontComponent)) { + return null; + } + + return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent); + } + + async create( + input: CreateFrontComponentInput, + workspaceId: string, + ): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const flatFrontComponentToCreate = + fromCreateFrontComponentInputToFlatFrontComponentToCreate({ + createFrontComponentInput: input, + workspaceId, + applicationId: workspaceCustomFlatApplication.id, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + frontComponent: { + flatEntityToCreate: [flatFrontComponentToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while creating front component', + ); + } + + const { flatFrontComponentMaps: recomputedFlatFrontComponentMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFrontComponentMaps'], + }, + ); + + return fromFlatFrontComponentToFrontComponentDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatFrontComponentToCreate.id, + flatEntityMaps: recomputedFlatFrontComponentMaps, + }), + ); + } + + async update( + input: UpdateFrontComponentInput, + workspaceId: string, + ): Promise { + const { flatFrontComponentMaps: existingFlatFrontComponentMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFrontComponentMaps'], + }, + ); + + const flatFrontComponentToUpdate = + fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow({ + flatFrontComponentMaps: existingFlatFrontComponentMaps, + updateFrontComponentInput: input, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + frontComponent: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [flatFrontComponentToUpdate], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while updating front component', + ); + } + + const { flatFrontComponentMaps: recomputedFlatFrontComponentMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFrontComponentMaps'], + }, + ); + + return fromFlatFrontComponentToFrontComponentDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: input.id, + flatEntityMaps: recomputedFlatFrontComponentMaps, + }), + ); + } + + async delete(id: string, workspaceId: string): Promise { + const { flatFrontComponentMaps: existingFlatFrontComponentMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFrontComponentMaps'], + }, + ); + + const flatFrontComponentToDelete = + fromDeleteFrontComponentInputToFlatFrontComponentOrThrow({ + flatFrontComponentMaps: existingFlatFrontComponentMaps, + frontComponentId: id, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + frontComponent: { + flatEntityToCreate: [], + flatEntityToDelete: [flatFrontComponentToDelete], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting front component', + ); + } + + return fromFlatFrontComponentToFrontComponentDto( + flatFrontComponentToDelete, + ); + } + + async findByIdOrThrow( + id: string, + workspaceId: string, + ): Promise { + const frontComponent = await this.findById(id, workspaceId); + + if (!isDefined(frontComponent)) { + throw new FrontComponentException( + 'Front component not found', + FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, + ); + } + + return frontComponent; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor.ts new file mode 100644 index 00000000000..fffe3dd5138 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor.ts @@ -0,0 +1,24 @@ +import { + type CallHandler, + type ExecutionContext, + Injectable, + type NestInterceptor, +} from '@nestjs/common'; + +import { type Observable, catchError } from 'rxjs'; + +import { frontComponentGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/front-component/utils/front-component-graphql-api-exception-handler.util'; + +@Injectable() +export class FrontComponentGraphqlApiExceptionInterceptor + implements NestInterceptor +{ + intercept( + _context: ExecutionContext, + next: CallHandler, + ): Observable { + return next + .handle() + .pipe(catchError(frontComponentGraphqlApiExceptionHandler)); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/utils/front-component-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/utils/front-component-graphql-api-exception-handler.util.ts new file mode 100644 index 00000000000..b33f42a2d35 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/utils/front-component-graphql-api-exception-handler.util.ts @@ -0,0 +1,29 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + ConflictError, + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + FrontComponentException, + FrontComponentExceptionCode, +} from 'src/engine/metadata-modules/front-component/front-component.exception'; + +export const frontComponentGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof FrontComponentException) { + switch (error.code) { + case FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND: + throw new NotFoundError(error); + case FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT: + throw new UserInputError(error); + case FrontComponentExceptionCode.FRONT_COMPONENT_ALREADY_EXISTS: + throw new ConflictError(error); + default: { + return assertUnreachable(error.code); + } + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts index 5238f2d8bd3..79b8e5f11a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts @@ -7,6 +7,7 @@ import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module'; import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; @@ -22,6 +23,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work imports: [ DataSourceModule, FieldMetadataModule, + FrontComponentModule, ObjectMetadataModule, SearchFieldMetadataModule, ServerlessFunctionModule, @@ -42,6 +44,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work exports: [ DataSourceModule, FieldMetadataModule, + FrontComponentModule, ObjectMetadataModule, SearchFieldMetadataModule, ServerlessFunctionModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/skill/dtos/create-skill.input.ts b/packages/twenty-server/src/engine/metadata-modules/skill/dtos/create-skill.input.ts index 62a654af89d..668b157c248 100644 --- a/packages/twenty-server/src/engine/metadata-modules/skill/dtos/create-skill.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/skill/dtos/create-skill.input.ts @@ -1,9 +1,16 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @InputType() export class CreateSkillInput { + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + @IsString() @IsNotEmpty() @Field() diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index e49e1ee3243..6140171f39f 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -40,6 +40,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { 'flat-maps:row-level-permission-predicate', flatRowLevelPermissionPredicateGroupMaps: 'flat-maps:row-level-permission-predicate-group', + flatFrontComponentMaps: 'flat-maps:front-component', } as const satisfies Record; export type AdditionalCacheDataMaps = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index d35fb4f3682..1a9198b0204 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -18,6 +18,7 @@ import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspa import { WorkspaceMigrationCronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/workspace-migration-cron-trigger-action-builder.service'; import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/workspace-migration-database-event-trigger-actions-builder.service'; import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service'; +import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service'; import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service'; import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service'; import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service'; @@ -60,6 +61,7 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationPageLayoutTabActionsBuilderService: WorkspaceMigrationPageLayoutTabActionsBuilderService, private readonly workspaceMigrationRowLevelPermissionPredicateActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService, private readonly workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService, + private readonly workspaceMigrationFrontComponentActionsBuilderService: WorkspaceMigrationFrontComponentActionsBuilderService, ) {} private setupOptimisticCache({ @@ -159,6 +161,7 @@ export class WorkspaceMigrationBuildOrchestratorService { flatPageLayoutMaps, flatPageLayoutWidgetMaps, flatPageLayoutTabMaps, + flatFrontComponentMaps, } = fromToAllFlatEntityMaps; if (isDefined(flatObjectMetadataMaps)) { @@ -928,6 +931,39 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatFrontComponentMaps)) { + const { from: fromFlatFrontComponentMaps, to: toFlatFrontComponentMaps } = + flatFrontComponentMaps; + + const frontComponentResult = + await this.workspaceMigrationFrontComponentActionsBuilderService.validateAndBuild( + { + additionalCacheDataMaps, + from: fromFlatFrontComponentMaps, + to: toFlatFrontComponentMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: undefined, + workspaceId, + }, + ); + + this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( + { + allFlatEntityMaps: optimisticAllFlatEntityMaps, + flatEntityMapsAndRelatedFlatEntityMaps: + frontComponentResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, + }, + ); + + if (frontComponentResult.status === 'fail') { + orchestratorFailureReport.frontComponent.push( + ...frontComponentResult.errors, + ); + } else { + orchestratorActionsReport.frontComponent = frontComponentResult.actions; + } + } + const allErrors = Object.values(orchestratorFailureReport); if (allErrors.some((report) => report.length > 0)) { @@ -1069,6 +1105,12 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.rowLevelPermissionPredicate .update, /// + + // Front components + ...aggregatedOrchestratorActionsReport.frontComponent.delete, + ...aggregatedOrchestratorActionsReport.frontComponent.create, + ...aggregatedOrchestratorActionsReport.frontComponent.update, + /// ], workspaceId, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type.ts new file mode 100644 index 00000000000..fe18c355789 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type.ts @@ -0,0 +1,12 @@ +import { type BaseCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type'; +import { type BaseDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type'; +import { type BaseUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type'; + +export type CreateFrontComponentAction = + BaseCreateWorkspaceMigrationAction<'frontComponent'>; + +export type UpdateFrontComponentAction = + BaseUpdateWorkspaceMigrationAction<'frontComponent'>; + +export type DeleteFrontComponentAction = + BaseDeleteWorkspaceMigrationAction<'frontComponent'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service.ts new file mode 100644 index 00000000000..d102284427f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { UpdateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type'; +import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type'; +import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type'; +import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-result.type'; +import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service'; + +@Injectable() +export class WorkspaceMigrationFrontComponentActionsBuilderService extends WorkspaceEntityMigrationBuilderService< + typeof ALL_METADATA_NAME.frontComponent +> { + constructor( + private readonly flatFrontComponentValidatorService: FlatFrontComponentValidatorService, + ) { + super(ALL_METADATA_NAME.frontComponent); + } + + protected validateFlatEntityCreation( + args: FlatEntityValidationArgs, + ): FlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.frontComponent, + 'create' + > { + const validationResult = + this.flatFrontComponentValidatorService.validateFlatFrontComponentCreation( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatFrontComponentToValidate } = args; + + return { + status: 'success', + action: { + type: 'create', + metadataName: 'frontComponent', + flatEntity: flatFrontComponentToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: FlatEntityValidationArgs, + ): FlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.frontComponent, + 'delete' + > { + const validationResult = + this.flatFrontComponentValidatorService.validateFlatFrontComponentDeletion( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatFrontComponentToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete', + metadataName: 'frontComponent', + entityId: flatFrontComponentToValidate.id, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.frontComponent + >, + ): FlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.frontComponent, + 'update' + > { + const validationResult = + this.flatFrontComponentValidatorService.validateFlatFrontComponentUpdate( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityId, flatEntityUpdates } = args; + + const updateFrontComponentAction: UpdateFrontComponentAction = { + type: 'update', + metadataName: 'frontComponent', + entityId: flatEntityId, + updates: flatEntityUpdates, + }; + + return { + status: 'success', + action: updateFrontComponentAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service.ts new file mode 100644 index 00000000000..3429c36c4a7 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service.ts @@ -0,0 +1,113 @@ +import { Injectable } from '@nestjs/common'; + +import { msg, t } from '@lingui/core/macro'; +import { isNonEmptyString } from '@sniptt/guards'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { FrontComponentExceptionCode } from 'src/engine/metadata-modules/front-component/front-component.exception'; +import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; +import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util'; +import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type'; +import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type'; + +@Injectable() +export class FlatFrontComponentValidatorService { + public validateFlatFrontComponentCreation({ + flatEntityToValidate: flatFrontComponent, + }: FlatEntityValidationArgs< + typeof ALL_METADATA_NAME.frontComponent + >): FailedFlatEntityValidation<'frontComponent', 'create'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + id: flatFrontComponent.id, + universalIdentifier: flatFrontComponent.universalIdentifier, + name: flatFrontComponent.name, + }, + metadataName: 'frontComponent', + type: 'create', + }); + + if (!isNonEmptyString(flatFrontComponent.name)) { + validationResult.errors.push({ + code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT, + message: t`Front component name is required`, + userFriendlyMessage: msg`Front component name is required`, + }); + } + + return validationResult; + } + + public validateFlatFrontComponentDeletion({ + flatEntityToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatFrontComponentMaps: optimisticFlatFrontComponentMaps, + }, + }: FlatEntityValidationArgs< + typeof ALL_METADATA_NAME.frontComponent + >): FailedFlatEntityValidation<'frontComponent', 'delete'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + id: flatEntityToValidate.id, + universalIdentifier: flatEntityToValidate.universalIdentifier, + name: flatEntityToValidate.name, + }, + metadataName: 'frontComponent', + type: 'delete', + }); + + const existingFrontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatEntityToValidate.id, + flatEntityMaps: optimisticFlatFrontComponentMaps, + }); + + if (!isDefined(existingFrontComponent)) { + validationResult.errors.push({ + code: FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, + message: t`Front component not found`, + userFriendlyMessage: msg`Front component not found`, + }); + + return validationResult; + } + + return validationResult; + } + + public validateFlatFrontComponentUpdate({ + flatEntityId, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatFrontComponentMaps: optimisticFlatFrontComponentMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.frontComponent + >): FailedFlatEntityValidation<'frontComponent', 'update'> { + const fromFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId, + flatEntityMaps: optimisticFlatFrontComponentMaps, + }); + + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + id: flatEntityId, + universalIdentifier: fromFlatFrontComponent?.universalIdentifier, + }, + metadataName: 'frontComponent', + type: 'update', + }); + + if (!isDefined(fromFlatFrontComponent)) { + validationResult.errors.push({ + code: FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, + message: t`Front component not found`, + userFriendlyMessage: msg`Front component not found`, + }); + + return validationResult; + } + + return validationResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts index 318f53b2ab7..cf560137a29 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts @@ -19,6 +19,7 @@ import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service'; import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-serverless-function-validator.service'; import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service'; +import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service'; import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service'; import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-group-validator.service'; import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service'; @@ -51,6 +52,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace FlatPageLayoutTabValidatorService, FlatRowLevelPermissionPredicateValidatorService, FlatRowLevelPermissionPredicateGroupValidatorService, + FlatFrontComponentValidatorService, ], exports: [ FlatViewValidatorService, @@ -75,6 +77,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace FlatPageLayoutTabValidatorService, FlatRowLevelPermissionPredicateValidatorService, FlatRowLevelPermissionPredicateGroupValidatorService, + FlatFrontComponentValidatorService, ], }) export class WorkspaceMigrationBuilderValidatorsModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts index 5323879d714..66e3f0636b8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts @@ -18,6 +18,7 @@ import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service'; import { WorkspaceMigrationServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/workspace-migration-serverless-function-actions-builder.service'; import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service'; +import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service'; import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service'; import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service'; @@ -50,6 +51,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationPageLayoutTabActionsBuilderService, WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService, WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService, + WorkspaceMigrationFrontComponentActionsBuilderService, ], exports: [ WorkspaceMigrationViewActionsBuilderService, @@ -74,6 +76,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService, WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService, FlatFieldMetadataTypeValidatorService, + WorkspaceMigrationFrontComponentActionsBuilderService, ], }) export class WorkspaceMigrationBuilderModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts new file mode 100644 index 00000000000..34aedf3ce31 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; +import { CreateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreateFrontComponentActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create', + 'frontComponent', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { flatEntity } = action; + + const frontComponentRepository = + queryRunner.manager.getRepository( + FrontComponentEntity, + ); + + await frontComponentRepository.insert({ + ...flatEntity, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service.ts new file mode 100644 index 00000000000..67373ed4836 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; +import { DeleteFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteFrontComponentActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete', + 'frontComponent', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { entityId } = action; + + const frontComponentRepository = + queryRunner.manager.getRepository( + FrontComponentEntity, + ); + + await frontComponentRepository.delete({ id: entityId, workspaceId }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service.ts new file mode 100644 index 00000000000..72bb30dc657 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; +import { UpdateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; +import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; + +@Injectable() +export class UpdateFrontComponentActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update', + 'frontComponent', +) { + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { entityId, updates } = action; + + const frontComponentRepository = + queryRunner.manager.getRepository( + FrontComponentEntity, + ); + + await frontComponentRepository.update( + { id: entityId, workspaceId }, + fromFlatEntityPropertiesUpdatesToPartialFlatEntity({ + updates, + }), + ); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index df9d06ae548..2d2206529d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -48,6 +48,9 @@ import { DeleteServerlessFunctionActionHandlerService } from 'src/engine/workspa import { UpdateServerlessFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service'; import { CreateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service'; import { DeleteSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/delete-skill-action-handler.service'; +import { CreateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service'; +import { UpdateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service'; +import { DeleteFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service'; import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/update-skill-action-handler.service'; import { CreateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service'; import { DeleteViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/delete-view-field-action-handler.service'; @@ -151,6 +154,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor CreateRowLevelPermissionPredicateGroupActionHandlerService, UpdateRowLevelPermissionPredicateGroupActionHandlerService, DeleteRowLevelPermissionPredicateGroupActionHandlerService, + + CreateFrontComponentActionHandlerService, + UpdateFrontComponentActionHandlerService, + DeleteFrontComponentActionHandlerService, ], }) export class WorkspaceSchemaMigrationRunnerActionHandlersModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts index 716c82e862a..924fb9fb630 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts @@ -68,6 +68,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = < case 'skill': case 'pageLayout': case 'pageLayoutWidget': + case 'frontComponent': case 'pageLayoutTab': { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({ flatEntity: action.flatEntity, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts index bde51d585e8..c5e30ddd2f0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts @@ -45,6 +45,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = < case 'skill': case 'pageLayout': case 'pageLayoutWidget': + case 'frontComponent': case 'pageLayoutTab': { const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow< MetadataFlatEntity diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts index dc0bc7912c5..c7cda3dd30d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts @@ -67,7 +67,8 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = < case 'skill': case 'pageLayout': case 'pageLayoutWidget': - case 'pageLayoutTab': { + case 'pageLayoutTab': + case 'frontComponent': { const flatEntityMapsKey = getMetadataFlatEntityMapsKey( action.metadataName, ); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..9bbf4c1ff50 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-creation.integration-spec.ts.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Front component creation should fail when name is empty 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "frontComponent": [ + { + "errors": [ + { + "code": "INVALID_FRONT_COMPONENT_INPUT", + "message": "Front component name is required", + "userFriendlyMessage": "Front component name is required", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "", + "universalIdentifier": Any, + }, + "metadataName": "frontComponent", + "status": "fail", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 frontComponent", + "summary": { + "frontComponent": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Multiple validation errors occurred while creating front component", + "name": "GraphQLError", +} +`; + +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": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "frontComponent": [ + { + "errors": [ + { + "code": "INVALID_FRONT_COMPONENT_INPUT", + "message": "Front component name is required", + "userFriendlyMessage": "Front component name is required", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "", + "universalIdentifier": Any, + }, + "metadataName": "frontComponent", + "status": "fail", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 frontComponent", + "summary": { + "frontComponent": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Multiple validation errors occurred while creating front component", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..00b996b093c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-deletion.integration-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Front component deletion should fail when front component does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "FRONT_COMPONENT_NOT_FOUND", + "userFriendlyMessage": "Front component not found.", + }, + "message": "Front component not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-update.integration-spec.ts.snap new file mode 100644 index 00000000000..ef8437738f6 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/__snapshots__/failing-front-component-update.integration-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Front component update should fail when front component does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "FRONT_COMPONENT_NOT_FOUND", + "userFriendlyMessage": "Front component not found.", + }, + "message": "Front component not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-creation.integration-spec.ts new file mode 100644 index 00000000000..01ab086ec45 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-creation.integration-spec.ts @@ -0,0 +1,49 @@ +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 { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input'; + +type TestContext = { + name: string | null; +}; + +const FAILING_TEST_CASES: EachTestingContext[] = [ + { + title: 'when name is empty', + context: { + name: '', + }, + }, + { + title: 'when name is whitespace-only', + context: { + name: ' ', + }, + }, + { + title: 'when name is too long', + context: { + name: null, + }, + }, +]; + +describe('Front component creation should fail', () => { + it.each(eachTestingContextFilter(FAILING_TEST_CASES))( + '$title', + async ({ context }) => { + const { errors } = await createFrontComponent({ + expectToFail: true, + input: { + name: context.name, + } as CreateFrontComponentInput, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-deletion.integration-spec.ts new file mode 100644 index 00000000000..1e0434b6c7e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-deletion.integration-spec.ts @@ -0,0 +1,34 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { deleteFrontComponent } from 'test/integration/metadata/suites/front-component/utils/delete-front-component.util'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; +import { v4 } from 'uuid'; + +type TestContext = { + id: string; +}; + +const FAILING_TEST_CASES: EachTestingContext[] = [ + { + title: 'when front component does not exist', + context: { + id: v4(), + }, + }, +]; + +describe('Front component deletion should fail', () => { + it.each(eachTestingContextFilter(FAILING_TEST_CASES))( + '$title', + async ({ context }) => { + const { errors } = await deleteFrontComponent({ + expectToFail: true, + input: { id: context.id }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-update.integration-spec.ts new file mode 100644 index 00000000000..8a5369cdb3e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/failing-front-component-update.integration-spec.ts @@ -0,0 +1,41 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateFrontComponent } from 'test/integration/metadata/suites/front-component/utils/update-front-component.util'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; +import { v4 } from 'uuid'; + +type TestContext = { + id: string; + name: string; +}; + +const FAILING_TEST_CASES: EachTestingContext[] = [ + { + title: 'when front component does not exist', + context: { + id: v4(), + name: 'updatedName', + }, + }, +]; + +describe('Front component update should fail', () => { + it.each(eachTestingContextFilter(FAILING_TEST_CASES))( + '$title', + async ({ context }) => { + const { errors } = await updateFrontComponent({ + expectToFail: true, + input: { + id: context.id, + update: { + name: context.name, + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-creation.integration-spec.ts new file mode 100644 index 00000000000..0e808668900 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-creation.integration-spec.ts @@ -0,0 +1,48 @@ +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'; + +describe('Front component creation should succeed', () => { + let createdFrontComponentId: string | undefined; + + afterEach(async () => { + if (createdFrontComponentId) { + await deleteFrontComponent({ + expectToFail: false, + input: { id: createdFrontComponentId }, + }); + createdFrontComponentId = undefined; + } + }); + + it('should create a basic front component with minimal input', async () => { + const { data } = await createFrontComponent({ + expectToFail: false, + input: { + name: 'testFrontComponent', + }, + }); + + createdFrontComponentId = data?.createFrontComponent?.id; + + expect(data.createFrontComponent).toMatchObject({ + id: expect.any(String), + name: 'testFrontComponent', + }); + }); + + it('should sanitize input by trimming whitespace', async () => { + const { data } = await createFrontComponent({ + expectToFail: false, + input: { + name: ' frontComponentWithSpaces ', + }, + }); + + createdFrontComponentId = data?.createFrontComponent?.id; + + expect(data.createFrontComponent).toMatchObject({ + id: expect.any(String), + name: 'frontComponentWithSpaces', + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-deletion.integration-spec.ts new file mode 100644 index 00000000000..7c088e7bdab --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-deletion.integration-spec.ts @@ -0,0 +1,40 @@ +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'; + +describe('Front component deletion should succeed', () => { + it('should successfully delete a front component', async () => { + const { data: createData } = await createFrontComponent({ + expectToFail: false, + input: { + name: 'frontComponentToDelete', + }, + }); + + const frontComponentId = createData.createFrontComponent.id; + + const { data: findBeforeData } = await findFrontComponent({ + expectToFail: false, + input: { id: frontComponentId }, + }); + + expect(findBeforeData.frontComponent.id).toBe(frontComponentId); + + const { data: deleteData } = await deleteFrontComponent({ + expectToFail: false, + input: { id: frontComponentId }, + }); + + expect(deleteData.deleteFrontComponent).toMatchObject({ + id: frontComponentId, + name: 'frontComponentToDelete', + }); + + const { data: findAfterData } = await findFrontComponent({ + expectToFail: false, + input: { id: frontComponentId }, + }); + + expect(findAfterData.frontComponent).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-update.integration-spec.ts new file mode 100644 index 00000000000..2d2b5d34a38 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/successful-front-component-update.integration-spec.ts @@ -0,0 +1,50 @@ +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 { 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; + + beforeEach(async () => { + const { data } = await createFrontComponent({ + expectToFail: false, + input: { + name: 'testFrontComponentToUpdate', + }, + }); + + testFrontComponentId = data.createFrontComponent.id; + }); + + afterEach(async () => { + if (isDefined(testFrontComponentId)) { + await deleteFrontComponent({ + expectToFail: false, + input: { id: testFrontComponentId }, + }); + testFrontComponentId = undefined; + } + }); + + it('should update front component name', async () => { + if (!isDefined(testFrontComponentId)) { + throw new Error('testFrontComponentId should be defined'); + } + + const { data } = await updateFrontComponent({ + expectToFail: false, + input: { + id: testFrontComponentId, + update: { + name: 'updatedFrontComponentName', + }, + }, + }); + + expect(data.updateFrontComponent).toMatchObject({ + id: testFrontComponentId, + name: 'updatedFrontComponentName', + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/create-front-component-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/create-front-component-query-factory.util.ts new file mode 100644 index 00000000000..77ee3144ead --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/create-front-component-query-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input'; + +export type CreateFrontComponentFactoryInput = CreateFrontComponentInput; + +const DEFAULT_FRONT_COMPONENT_GQL_FIELDS = ` + id + name + applicationId + createdAt + updatedAt +`; + +export const createFrontComponentQueryFactory = ({ + input, + gqlFields = DEFAULT_FRONT_COMPONENT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateFrontComponent($input: CreateFrontComponentInput!) { + createFrontComponent(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/create-front-component.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/create-front-component.util.ts new file mode 100644 index 00000000000..67fbd3283ac --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/create-front-component.util.ts @@ -0,0 +1,43 @@ +import { + type CreateFrontComponentFactoryInput, + createFrontComponentQueryFactory, +} from 'test/integration/metadata/suites/front-component/utils/create-front-component-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; + +export const createFrontComponent = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createFrontComponent: FrontComponentDTO; +}> => { + const graphqlOperation = createFrontComponentQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Front component creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Front component creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/delete-front-component-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/delete-front-component-query-factory.util.ts new file mode 100644 index 00000000000..7d1a63a083f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/delete-front-component-query-factory.util.ts @@ -0,0 +1,27 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DeleteFrontComponentFactoryInput = { + id: string; +}; + +const DEFAULT_FRONT_COMPONENT_GQL_FIELDS = ` + id + name +`; + +export const deleteFrontComponentQueryFactory = ({ + input, + gqlFields = DEFAULT_FRONT_COMPONENT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeleteFrontComponent($id: UUID!) { + deleteFrontComponent(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/delete-front-component.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/delete-front-component.util.ts new file mode 100644 index 00000000000..17b3c61eee2 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/delete-front-component.util.ts @@ -0,0 +1,43 @@ +import { + type DeleteFrontComponentFactoryInput, + deleteFrontComponentQueryFactory, +} from 'test/integration/metadata/suites/front-component/utils/delete-front-component-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; + +export const deleteFrontComponent = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deleteFrontComponent: FrontComponentDTO; +}> => { + const graphqlOperation = deleteFrontComponentQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Front component deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Front component deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-component-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-component-query-factory.util.ts new file mode 100644 index 00000000000..5b8f6194ca5 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-component-query-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindFrontComponentFactoryInput = { + id: string; +}; + +const DEFAULT_FRONT_COMPONENT_GQL_FIELDS = ` + id + name + applicationId + createdAt + updatedAt +`; + +export const findFrontComponentQueryFactory = ({ + input, + gqlFields = DEFAULT_FRONT_COMPONENT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query FrontComponent($id: UUID!) { + frontComponent(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-component.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-component.util.ts new file mode 100644 index 00000000000..09daeaa2869 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-component.util.ts @@ -0,0 +1,43 @@ +import { + type FindFrontComponentFactoryInput, + findFrontComponentQueryFactory, +} from 'test/integration/metadata/suites/front-component/utils/find-front-component-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; + +export const findFrontComponent = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + frontComponent: FrontComponentDTO; +}> => { + const graphqlOperation = findFrontComponentQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Finding front component should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Finding front component has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-components-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-components-query-factory.util.ts new file mode 100644 index 00000000000..9ba70d82941 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-components-query-factory.util.ts @@ -0,0 +1,22 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +const DEFAULT_FRONT_COMPONENT_GQL_FIELDS = ` + id + name + applicationId + createdAt + updatedAt +`; + +export const findFrontComponentsQueryFactory = ({ + gqlFields = DEFAULT_FRONT_COMPONENT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query FrontComponents { + frontComponents { + ${gqlFields} + } + } + `, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-components.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-components.util.ts new file mode 100644 index 00000000000..3d8ff86c2cf --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/find-front-components.util.ts @@ -0,0 +1,39 @@ +import { findFrontComponentsQueryFactory } from 'test/integration/metadata/suites/front-component/utils/find-front-components-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; + +export const findFrontComponents = async ({ + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + frontComponents: FrontComponentDTO[]; +}> => { + const graphqlOperation = findFrontComponentsQueryFactory({ + gqlFields, + input: undefined, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Finding front components should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Finding front components has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/update-front-component-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/update-front-component-query-factory.util.ts new file mode 100644 index 00000000000..cbed940530f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/update-front-component-query-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input'; + +export type UpdateFrontComponentFactoryInput = UpdateFrontComponentInput; + +const DEFAULT_FRONT_COMPONENT_GQL_FIELDS = ` + id + name + applicationId + createdAt + updatedAt +`; + +export const updateFrontComponentQueryFactory = ({ + gqlFields = DEFAULT_FRONT_COMPONENT_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdateFrontComponent($input: UpdateFrontComponentInput!) { + updateFrontComponent(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/front-component/utils/update-front-component.util.ts b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/update-front-component.util.ts new file mode 100644 index 00000000000..18e9bed2db3 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/front-component/utils/update-front-component.util.ts @@ -0,0 +1,43 @@ +import { + type UpdateFrontComponentFactoryInput, + updateFrontComponentQueryFactory, +} from 'test/integration/metadata/suites/front-component/utils/update-front-component-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; + +export const updateFrontComponent = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updateFrontComponent: FrontComponentDTO; +}> => { + const graphqlOperation = updateFrontComponentQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Front component update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Front component update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/skill/successful-skill-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/skill/successful-skill-deletion.integration-spec.ts index 641d882ccb1..2b0c3b3a27e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/skill/successful-skill-deletion.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/skill/successful-skill-deletion.integration-spec.ts @@ -1,6 +1,7 @@ import { createSkill } from 'test/integration/metadata/suites/skill/utils/create-skill.util'; import { deleteSkill } from 'test/integration/metadata/suites/skill/utils/delete-skill.util'; import { findSkill } from 'test/integration/metadata/suites/skill/utils/find-skill.util'; +import { isDefined } from 'twenty-shared/utils'; describe('Skill deletion should succeed', () => { it('should successfully delete a custom skill', async () => { @@ -15,6 +16,10 @@ describe('Skill deletion should succeed', () => { const skillId = createData.createSkill.id; + if (!isDefined(skillId)) { + return; + } + const { data: findBeforeData } = await findSkill({ expectToFail: false, input: { id: skillId }, diff --git a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts index 3f34d17484c..6a42d664905 100644 --- a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts +++ b/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts @@ -20,4 +20,5 @@ export const ALL_METADATA_NAME = { pageLayout: 'pageLayout', pageLayoutWidget: 'pageLayoutWidget', pageLayoutTab: 'pageLayoutTab', + frontComponent: 'frontComponent', } as const;