diff --git a/packages/twenty-apps/examples/postcard/package.json b/packages/twenty-apps/examples/postcard/package.json index 52fe790b89..50b30818c2 100644 --- a/packages/twenty-apps/examples/postcard/package.json +++ b/packages/twenty-apps/examples/postcard/package.json @@ -19,8 +19,8 @@ "test:watch": "vitest" }, "dependencies": { - "twenty-client-sdk": "0.9.0", - "twenty-sdk": "0.9.0" + "twenty-client-sdk": "2.2.0", + "twenty-sdk": "2.2.0" }, "devDependencies": { "@types/node": "^24.7.2", diff --git a/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx b/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx index e34358621d..4dd93b41f5 100644 --- a/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx +++ b/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { defineFrontComponent } from 'twenty-sdk/define'; -import { useSelectedRecordIds } from 'twenty-sdk/front-component'; +import { useRecordId } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; import { isDefined } from 'twenty-shared/utils'; @@ -72,8 +72,7 @@ const CardDisplay = ({ }; const PostCardPreview = () => { - const selectedRecordIds = useSelectedRecordIds(); - const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null; + const recordId = useRecordId(); const [postCard, setPostCard] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/packages/twenty-apps/examples/postcard/src/components/generate-post-card-component-effect.tsx b/packages/twenty-apps/examples/postcard/src/components/generate-post-card-component-effect.tsx index 33a0135887..66b3a64467 100644 --- a/packages/twenty-apps/examples/postcard/src/components/generate-post-card-component-effect.tsx +++ b/packages/twenty-apps/examples/postcard/src/components/generate-post-card-component-effect.tsx @@ -1,6 +1,11 @@ import { useEffect } from 'react'; import { defineFrontComponent } from 'twenty-sdk/define'; -import { useSelectedRecordIds, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component'; +import { + enqueueSnackbar, + unmountFrontComponent, + updateProgress, + useRecordId, +} from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; @@ -10,8 +15,7 @@ const SYSTEM_PROMPT = 'text, nothing else — no greeting label, no sign-off label, just the message.'; const GeneratePostCardEffect = () => { - const selectedRecordIds = useSelectedRecordIds(); - const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null; + const recordId = useRecordId(); useEffect(() => { if (recordId === null) { diff --git a/packages/twenty-apps/examples/postcard/src/components/send-post-cards-component-effect.tsx b/packages/twenty-apps/examples/postcard/src/components/send-post-cards-component-effect.tsx index e72dcd523c..88ac8e826a 100644 --- a/packages/twenty-apps/examples/postcard/src/components/send-post-cards-component-effect.tsx +++ b/packages/twenty-apps/examples/postcard/src/components/send-post-cards-component-effect.tsx @@ -1,50 +1,42 @@ import { useEffect } from 'react'; import { defineFrontComponent } from 'twenty-sdk/define'; -import { useSelectedRecordIds, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component'; +import { + enqueueSnackbar, + unmountFrontComponent, + updateProgress, + useRecordId, +} from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; const SendPostCardsEffect = () => { - const selectedRecordIds = useSelectedRecordIds(); + const recordId = useRecordId(); useEffect(() => { const send = async () => { try { - if (selectedRecordIds.length === 0) { - await enqueueSnackbar({ - message: 'No postcards selected', - variant: 'error', - }); - await unmountFrontComponent(); - return; - } - await updateProgress(0.1); const client = new CoreApiClient(); await updateProgress(0.3); - for (let i = 0; i < selectedRecordIds.length; i++) { + if (recordId) { await client.mutation({ updatePostCard: { __args: { - id: selectedRecordIds[i], + id: recordId, data: { status: 'SENT' }, }, id: true, }, }); - await updateProgress(0.3 + (0.7 * (i + 1)) / selectedRecordIds.length); + await enqueueSnackbar({ + message: `Postcard sent`, + variant: 'success', + }); } - const count = selectedRecordIds.length; - - await enqueueSnackbar({ - message: `${count} postcard${count > 1 ? 's' : ''} sent`, - variant: 'success', - }); - await unmountFrontComponent(); } catch (error) { const message = @@ -56,7 +48,7 @@ const SendPostCardsEffect = () => { }; send(); - }, [selectedRecordIds]); + }, [recordId]); return null; }; diff --git a/packages/twenty-apps/examples/postcard/yarn.lock b/packages/twenty-apps/examples/postcard/yarn.lock index 923597089b..cad4f03494 100644 --- a/packages/twenty-apps/examples/postcard/yarn.lock +++ b/packages/twenty-apps/examples/postcard/yarn.lock @@ -3317,8 +3317,8 @@ __metadata: oxlint: "npm:^0.16.0" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" - twenty-client-sdk: "npm:0.9.0" - twenty-sdk: "npm:0.9.0" + twenty-client-sdk: "npm:2.2.0" + twenty-sdk: "npm:2.2.0" typescript: "npm:^5.9.3" vite-tsconfig-paths: "npm:^4.2.1" vitest: "npm:^3.1.1" @@ -4059,21 +4059,21 @@ __metadata: languageName: node linkType: hard -"twenty-client-sdk@npm:0.9.0": - version: 0.9.0 - resolution: "twenty-client-sdk@npm:0.9.0" +"twenty-client-sdk@npm:2.2.0": + version: 2.2.0 + resolution: "twenty-client-sdk@npm:2.2.0" dependencies: "@genql/cli": "npm:^3.0.3" "@genql/runtime": "npm:^2.10.0" esbuild: "npm:^0.25.0" graphql: "npm:^16.8.1" - checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54 + checksum: 10c0/90122593efa53440ae386960211a2c274b13a410942bf6f9bc35cf952ae55fe83280e29074677fd284fa3c79069fc578980db06a92a143c08e043f865dbbbd3c languageName: node linkType: hard -"twenty-sdk@npm:0.9.0": - version: 0.9.0 - resolution: "twenty-sdk@npm:0.9.0" +"twenty-sdk@npm:2.2.0": + version: 2.2.0 + resolution: "twenty-sdk@npm:2.2.0" dependencies: "@genql/cli": "npm:^3.0.3" "@genql/runtime": "npm:^2.10.0" @@ -4093,7 +4093,7 @@ __metadata: react: "npm:^19.0.0" react-dom: "npm:^19.0.0" tinyglobby: "npm:^0.2.15" - twenty-client-sdk: "npm:0.9.0" + twenty-client-sdk: "npm:2.2.0" typescript: "npm:^5.9.2" uuid: "npm:^13.0.0" vite: "npm:^7.0.0" @@ -4101,7 +4101,7 @@ __metadata: zod: "npm:^4.1.11" bin: twenty: dist/cli.cjs - checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3 + checksum: 10c0/8978b4b0aa5ea282c8f76799347d9d1812901ec609bc2bd9270261a6bc5589ad361f6102a06900a4511a29a8378cd3811c2c0bad9f01cb8adadabca6d96dfd0b languageName: node linkType: hard diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 9a9ebdef2f..374b530b1a 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2872,6 +2872,7 @@ enum AllMetadataName { fieldPermission frontComponent webhook + applicationVariable connectionProvider } diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index c4423f814f..89380adc55 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2495,7 +2495,7 @@ export interface CollectionHash { __typename: 'CollectionHash' } -export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'connectionProvider' +export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider' export interface MinimalObjectMetadata { id: Scalars['UUID'] @@ -8854,6 +8854,7 @@ export const enumAllMetadataName = { fieldPermission: 'fieldPermission' as const, frontComponent: 'frontComponent' as const, webhook: 'webhook' as const, + applicationVariable: 'applicationVariable' as const, connectionProvider: 'connectionProvider' as const } diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 0a8f4ec07d..a8bf33b501 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -185,6 +185,7 @@ export type AiSystemPromptSection = { export enum AllMetadataName { agent = 'agent', + applicationVariable = 'applicationVariable', commandMenuItem = 'commandMenuItem', connectionProvider = 'connectionProvider', fieldMetadata = 'fieldMetadata', 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 432c45c2c2..270086b0c3 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 @@ -48,6 +48,7 @@ export const useMetadataErrorHandler = () => { navigationMenuItem: t`navigation menu item`, webhook: t`webhook`, viewSort: t`view sort`, + applicationVariable: t`application variable`, connectionProvider: t`connection provider`, } as const satisfies Record; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777966965587-transform-application-variable-to-syncable-entity.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777966965587-transform-application-variable-to-syncable-entity.ts new file mode 100644 index 0000000000..b4c4b5d614 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777966965587-transform-application-variable-to-syncable-entity.ts @@ -0,0 +1,33 @@ +import { QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +@RegisteredInstanceCommand('2.3.0', 1777966965587) +export class TransformApplicationVariableToSyncableEntityFastInstanceCommand + implements FastInstanceCommand +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'DROP INDEX "core"."IDX_78ae6cfe5f49a76c4bf842ad58"', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE"', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ADD "universalIdentifier" uuid', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" DROP COLUMN "universalIdentifier"', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE" UNIQUE ("key", "applicationId")', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_78ae6cfe5f49a76c4bf842ad58" ON "core"."applicationVariable" ("workspaceId") ', + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier.ts new file mode 100644 index 0000000000..8517def363 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier.ts @@ -0,0 +1,54 @@ +import { DataSource, QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface'; + +@RegisteredInstanceCommand('2.3.0', 1777966965588, { type: 'slow' }) +export class BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand + implements SlowInstanceCommand +{ + async runDataMigration(dataSource: DataSource): Promise { + await dataSource.query( + 'DELETE "core"."applicationVariable" WHERE "applicationId" IS NULL', + ); + await dataSource.query( + 'UPDATE "core"."applicationVariable" SET "universalIdentifier" = gen_random_uuid() WHERE "universalIdentifier" IS NULL', + ); + } + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830"', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "applicationId" SET NOT NULL', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "universalIdentifier" SET NOT NULL', + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX "IDX_44ecebdf70cbed17f89527b36b" ON "core"."applicationVariable" ("workspaceId", "universalIdentifier") ', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'DROP INDEX "core"."IDX_44ecebdf70cbed17f89527b36b"', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "universalIdentifier" DROP NOT NULL', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830"', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "applicationId" DROP NOT NULL', + ); + await queryRunner.query( + 'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index f527b66288..da7e8d1e1d 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -26,6 +26,8 @@ import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/ import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings'; import { ConnectionProviderSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777896012579-connection-provider-syncable-entity'; import { RemoveUserDefaultAvatarUrlFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url'; +import { TransformApplicationVariableToSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777966965587-transform-application-variable-to-syncable-entity'; +import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -54,4 +56,6 @@ export const INSTANCE_COMMANDS = [ MigrateToolTriggerSettingsSlowInstanceCommand, ConnectionProviderSyncableEntityFastInstanceCommand, RemoveUserDefaultAvatarUrlFastInstanceCommand, + TransformApplicationVariableToSyncableEntityFastInstanceCommand, + BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util.ts index d185e70ab4..3a4b1e0819 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util.ts @@ -1,7 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined, isPlainObject } from 'twenty-shared/utils'; -import { type CompositeFieldGroupByDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/types/composite-field-group-by-definition.type'; import { isGroupByDateFieldDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-group-by-date-field-definition.util'; import { isRelationNestedFieldSupportedInGroupBy } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-relation-nested-field-supported-in-group-by.util'; import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-single-key-for-group-by-or-throw.util'; diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.resolver.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.resolver.ts index e1a084d4a3..afeb68ac33 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.resolver.ts @@ -11,10 +11,6 @@ import { CreateApiKeyInput } from 'src/engine/core-modules/api-key/dtos/create-a import { GetApiKeyInput } from 'src/engine/core-modules/api-key/dtos/get-api-key.input'; import { RevokeApiKeyInput } from 'src/engine/core-modules/api-key/dtos/revoke-api-key.input'; import { UpdateApiKeyInput } from 'src/engine/core-modules/api-key/dtos/update-api-key.input'; -import { - ApiKeyException, - ApiKeyExceptionCode, -} from 'src/engine/core-modules/api-key/exceptions/api-key.exception'; import { apiKeyGraphqlApiExceptionHandler } from 'src/engine/core-modules/api-key/utils/api-key-graphql-api-exception-handler.util'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts index 65ac2d0196..fe5897ba41 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts @@ -16,7 +16,6 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util'; import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util'; -import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; @@ -32,7 +31,6 @@ export class ApplicationSyncService { constructor( private readonly applicationService: ApplicationService, - private readonly applicationVariableService: ApplicationVariableEntityService, private readonly applicationManifestMigrationService: ApplicationManifestMigrationService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceCacheService: WorkspaceCacheService, @@ -145,14 +143,6 @@ export class ApplicationSyncService { ); } - await this.applicationVariableService.upsertManyApplicationVariableEntities( - { - applicationVariables: manifest.application.applicationVariables, - applicationId: application.id, - workspaceId, - }, - ); - const resolvedRegistrationId = applicationRegistrationId ?? application.applicationRegistrationId; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util.ts new file mode 100644 index 0000000000..72b5a8bf3e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util.ts @@ -0,0 +1,31 @@ +import { type UniversalFlatApplicationVariable } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type'; + +export const fromApplicationVariableManifestToUniversalFlatApplicationVariable = + ({ + key, + universalIdentifier, + description, + value, + isSecret, + applicationUniversalIdentifier, + now, + }: { + key: string; + universalIdentifier: string; + description?: string; + value?: string; + isSecret?: boolean; + applicationUniversalIdentifier: string; + now: string; + }): UniversalFlatApplicationVariable => { + return { + universalIdentifier, + applicationUniversalIdentifier, + key, + value: isSecret ? '' : (value ?? ''), // We protect secret variable by not syncing its value at all + description: description ?? '', + isSecret: isSecret ?? false, + createdAt: now, + updatedAt: now, + }; + }; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts index 39eb07c8b6..0605c5de27 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts @@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util'; +import { fromApplicationVariableManifestToUniversalFlatApplicationVariable } from 'src/engine/core-modules/application/application-manifest/converters/from-application-variable-manifest-to-universal-flat-application-variable.util'; import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util'; import { fromConnectionProviderManifestToUniversalFlatConnectionProvider } from 'src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util'; import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util'; @@ -431,6 +432,28 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({ } } + for (const [key, applicationVariableManifest] of Object.entries( + manifest.application.applicationVariables ?? {}, + )) { + addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({ + universalFlatEntity: + fromApplicationVariableManifestToUniversalFlatApplicationVariable({ + key, + universalIdentifier: applicationVariableManifest.universalIdentifier, + value: + 'value' in applicationVariableManifest + ? applicationVariableManifest.value + : undefined, + description: applicationVariableManifest.description, + isSecret: applicationVariableManifest.isSecret, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityMapsToMutate: + allUniversalFlatEntityMaps.flatApplicationVariableMaps, + }); + } + for (const commandMenuItemManifest of manifest.commandMenuItems ?? []) { if (!isDefined(commandMenuItemManifest.frontComponentUniversalIdentifier)) { throw new Error( diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.resolver.ts index 89d0a9bb57..47fa498fa0 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.resolver.ts @@ -32,7 +32,6 @@ import { RotateClientSecretDTO } from 'src/engine/core-modules/application/appli import { TransferApplicationRegistrationOwnershipInput } from 'src/engine/core-modules/application/application-registration/dtos/transfer-application-registration-ownership.input'; import { UpdateApplicationRegistrationInput } from 'src/engine/core-modules/application/application-registration/dtos/update-application-registration.input'; import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum'; -import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts index 3fa36f8908..4d933fa37e 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/__tests__/application-variable.service.spec.ts @@ -159,220 +159,6 @@ describe('ApplicationVariableEntityService', () => { }); }); - describe('upsertManyApplicationVariableEntities', () => { - it('should encrypt secret values when creating new variables', async () => { - repository.find.mockResolvedValue([]); - repository.save.mockResolvedValue({} as any); - repository.delete.mockResolvedValue({ affected: 0 } as any); - - await service.upsertManyApplicationVariableEntities({ - applicationVariables: { - SECRET_KEY: { - universalIdentifier: 'secret-key-123', - value: 'my-secret', - description: 'A secret key', - isSecret: true, - }, - }, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }); - - expect(secretEncryptionService.encrypt).toHaveBeenCalledWith('my-secret'); - expect(repository.save).toHaveBeenCalledWith([ - { - key: 'SECRET_KEY', - value: 'encrypted_my-secret', - description: 'A secret key', - isSecret: true, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }, - ]); - }); - - it('should not encrypt non-secret values when creating new variables', async () => { - repository.find.mockResolvedValue([]); - repository.save.mockResolvedValue({} as any); - repository.delete.mockResolvedValue({ affected: 0 } as any); - - await service.upsertManyApplicationVariableEntities({ - applicationVariables: { - PUBLIC_URL: { - universalIdentifier: 'public-url-123', - value: 'https://example.com', - description: 'Public URL', - isSecret: false, - }, - }, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }); - - expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); - expect(repository.save).toHaveBeenCalledWith([ - { - key: 'PUBLIC_URL', - value: 'https://example.com', - description: 'Public URL', - isSecret: false, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }, - ]); - }); - - it('should handle undefined isSecret as false', async () => { - repository.find.mockResolvedValue([]); - repository.save.mockResolvedValue({} as any); - repository.delete.mockResolvedValue({ affected: 0 } as any); - - await service.upsertManyApplicationVariableEntities({ - applicationVariables: { - SOME_VAR: { - universalIdentifier: 'some-var-123', - value: 'some-value', - description: 'Some variable', - }, - }, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }); - - expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); - expect(repository.save).toHaveBeenCalledWith([ - expect.objectContaining({ - isSecret: false, - }), - ]); - }); - - it('should update existing variables without changing values', async () => { - const existingVariable = { - id: '1', - key: 'EXISTING_VAR', - value: 'existing-encrypted-value', - isSecret: true, - applicationId: mockApplicationId, - } as ApplicationVariableEntity; - - repository.find.mockResolvedValue([existingVariable]); - repository.save.mockResolvedValue({} as any); - repository.delete.mockResolvedValue({ affected: 0 } as any); - - await service.upsertManyApplicationVariableEntities({ - applicationVariables: { - EXISTING_VAR: { - universalIdentifier: 'existing-var-123', - value: 'new-value', - description: 'Updated description', - isSecret: true, - }, - }, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }); - - expect(repository.save).toHaveBeenCalledWith([ - { - id: '1', - description: 'Updated description', - isSecret: true, - }, - ]); - expect(repository.update).not.toHaveBeenCalled(); - }); - - it('should update existing value when shouldUpdateValue is true', async () => { - const existingVariable = { - id: '1', - key: 'EXISTING_VAR', - value: 'existing-encrypted-value', - isSecret: true, - applicationId: mockApplicationId, - } as ApplicationVariableEntity; - - repository.find.mockResolvedValue([existingVariable]); - repository.save.mockResolvedValue({} as any); - repository.delete.mockResolvedValue({ affected: 0 } as any); - - await service.upsertManyApplicationVariableEntities({ - applicationVariables: { - EXISTING_VAR: { - universalIdentifier: 'existing-var-123', - value: 'new-value', - description: 'Updated description', - isSecret: true, - }, - }, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - shouldUpdateValue: true, - }); - - expect(repository.save).toHaveBeenCalledWith([ - { - id: '1', - description: 'Updated description', - value: 'encrypted_new-value', - isSecret: true, - }, - ]); - }); - - it('should update existing value if isSecret changes', async () => { - const existingVariable = { - id: '1', - key: 'EXISTING_VAR', - value: 'existing-encrypted-value', - isSecret: true, - applicationId: mockApplicationId, - } as ApplicationVariableEntity; - - repository.find.mockResolvedValue([existingVariable]); - repository.save.mockResolvedValue({} as any); - repository.delete.mockResolvedValue({ affected: 0 } as any); - - await service.upsertManyApplicationVariableEntities({ - applicationVariables: { - EXISTING_VAR: { - universalIdentifier: 'existing-var-123', - value: 'new-value', - description: 'Updated description', - isSecret: false, - }, - }, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }); - - expect(repository.save).toHaveBeenCalledWith([ - { - id: '1', - description: 'Updated description', - value: 'new-value', - isSecret: false, - }, - ]); - expect(repository.update).not.toHaveBeenCalled(); - }); - - it('should handle undefined applicationVariables', async () => { - await service.upsertManyApplicationVariableEntities({ - applicationVariables: undefined, - applicationId: mockApplicationId, - workspaceId: mockWorkspaceId, - }); - - expect(repository.find).not.toHaveBeenCalled(); - expect(repository.save).not.toHaveBeenCalled(); - expect(repository.update).not.toHaveBeenCalled(); - expect( - workspaceCacheService.invalidateAndRecompute, - ).not.toHaveBeenCalled(); - }); - }); - describe('getDisplayValue', () => { it('should return plain value for non-secret variables', () => { const variable = { diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable-exception-filter.ts index 85fc7b63bd..ae6303781a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable-exception-filter.ts @@ -6,7 +6,10 @@ import { ApplicationVariableEntityException, ApplicationVariableEntityExceptionCode, } from 'src/engine/core-modules/application/application-variable/application-variable.exception'; -import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; @Catch(ApplicationVariableEntityException) export class ApplicationVariableEntityExceptionFilter @@ -16,6 +19,8 @@ export class ApplicationVariableEntityExceptionFilter switch (exception.code) { case ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND: throw new NotFoundError(exception); + case ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT: + throw new UserInputError(exception); default: assertUnreachable(exception.code); } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.entity.ts index 1e78e25bc7..8b571ec561 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.entity.ts @@ -5,41 +5,23 @@ import { Column, CreateDateColumn, Entity, - Index, - JoinColumn, - ManyToOne, PrimaryGeneratedColumn, - Unique, UpdateDateColumn, } from 'typeorm'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; -import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; -import type { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface'; +import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @Entity({ name: 'applicationVariable', schema: 'core', }) @ObjectType('ApplicationVariable') -@Unique('IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE', [ - 'key', - 'applicationId', -]) -export class ApplicationVariableEntity { +export class ApplicationVariableEntity extends SyncableEntity { @IDField(() => UUIDScalarType) @PrimaryGeneratedColumn('uuid') id: string; - @Column({ nullable: false, type: 'uuid' }) - @Index() - workspaceId: string; - - @ManyToOne('WorkspaceEntity', { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'workspaceId' }) - workspace: EntityRelation; - @Column({ nullable: false, type: 'text' }) key: string; @@ -52,20 +34,6 @@ export class ApplicationVariableEntity { @Column({ nullable: false, type: 'boolean', default: false }) isSecret: boolean; - @Column({ nullable: true, type: 'uuid' }) - applicationId?: string; - - @ManyToOne( - () => ApplicationEntity, - (application) => application.applicationVariables, - { - onDelete: 'CASCADE', - nullable: true, - }, - ) - @JoinColumn({ name: 'applicationId' }) - application: EntityRelation | null; - @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.exception.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.exception.ts index 2014a72de4..ef20294a6b 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.exception.ts @@ -6,6 +6,7 @@ import { CustomException } from 'src/utils/custom-exception'; export enum ApplicationVariableEntityExceptionCode { APPLICATION_VARIABLE_NOT_FOUND = 'APPLICATION_VARIABLE_NOT_FOUND', + INVALID_APPLICATION_VARIABLE_INPUT = 'INVALID_APPLICATION_VARIABLE_INPUT', } const getApplicationVariableEntityExceptionUserFriendlyMessage = ( @@ -14,6 +15,8 @@ const getApplicationVariableEntityExceptionUserFriendlyMessage = ( switch (code) { case ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND: return msg`Application variable not found.`; + case ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT: + return msg`Invalid application variable input.`; default: assertUnreachable(code); } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.module.ts index d82381b68f..124260a646 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.module.ts @@ -3,21 +3,24 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; import { ApplicationVariableEntityResolver } from 'src/engine/core-modules/application/application-variable/application-variable.resolver'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/application/application-variable/application-variable.service'; import { WorkspaceApplicationVariableMapCacheService } from 'src/engine/core-modules/application/application-variable/workspace-application-variable-map-cache.service'; import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; +import { FlatApplicationVariableModule } from 'src/engine/metadata-modules/flat-application-variable/flat-application-variable.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ NestjsQueryTypeOrmModule.forFeature([ApplicationVariableEntity]), - TypeOrmModule.forFeature([ApplicationVariableEntity]), + TypeOrmModule.forFeature([ApplicationVariableEntity, ApplicationEntity]), PermissionsModule, WorkspaceCacheModule, SecretEncryptionModule, + FlatApplicationVariableModule, ], providers: [ ApplicationVariableEntityService, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts index 321a0f1217..90cad0e4b3 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/application-variable.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ApplicationVariables } from 'twenty-shared/application'; import { isDefined } from 'twenty-shared/utils'; -import { In, Not, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; import { @@ -23,14 +22,6 @@ export class ApplicationVariableEntityService { private readonly secretEncryptionService: SecretEncryptionService, ) {} - private encryptSecretValue(value: string, isSecret: boolean): string { - if (!isSecret) { - return value; - } - - return this.secretEncryptionService.encrypt(value); - } - getDisplayValue(applicationVariable: ApplicationVariableEntity): string { if (!applicationVariable.isSecret) { return applicationVariable.value; @@ -42,48 +33,6 @@ export class ApplicationVariableEntityService { }); } - // Decrypted plaintext value. Server-side only — never expose via GraphQL. - // Used by trusted server flows that need the raw secret (e.g. exchanging an - // OAuth client secret with a third-party provider). - getRawValue(applicationVariable: ApplicationVariableEntity): string { - if (!applicationVariable.isSecret) { - return applicationVariable.value; - } - - return this.secretEncryptionService.decrypt(applicationVariable.value); - } - - async findOneByKey({ - applicationId, - key, - }: { - applicationId: string; - key: string; - }): Promise { - return this.applicationVariableRepository.findOne({ - where: { applicationId, key }, - }); - } - - async getRawValueByKeyOrThrow({ - applicationId, - key, - }: { - applicationId: string; - key: string; - }): Promise { - const variable = await this.findOneByKey({ applicationId, key }); - - if (!isDefined(variable)) { - throw new ApplicationVariableEntityException( - `Application variable "${key}" not found for application ${applicationId}`, - ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND, - ); - } - - return this.getRawValue(variable); - } - async update({ key, plainTextValue, @@ -105,10 +54,9 @@ export class ApplicationVariableEntityService { ); } - const encryptedValue = this.encryptSecretValue( - plainTextValue, - existingVariable.isSecret, - ); + const encryptedValue = existingVariable.isSecret + ? this.secretEncryptionService.encrypt(plainTextValue) + : plainTextValue; await this.applicationVariableRepository.update( { key, applicationId }, @@ -121,79 +69,4 @@ export class ApplicationVariableEntityService { 'applicationVariableMaps', ]); } - - async upsertManyApplicationVariableEntities({ - applicationVariables, - applicationId, - workspaceId, - shouldUpdateValue = false, - }: { - applicationVariables?: ApplicationVariables; - applicationId: string; - workspaceId: string; - shouldUpdateValue?: boolean; - }) { - if (!isDefined(applicationVariables)) { - return; - } - - const keys = Object.keys(applicationVariables); - - const existingVariables = await this.applicationVariableRepository.find({ - where: { - applicationId, - key: In(keys), - }, - }); - - const existingVariablesByKey = new Map( - existingVariables.map((variable) => [variable.key, variable]), - ); - - const entitiesToSave: Partial[] = []; - - for (const [key, { value, description, isSecret }] of Object.entries( - applicationVariables, - )) { - const existingVariable = existingVariablesByKey.get(key); - const isSecretValue = isSecret ?? false; - const encryptedValue = this.encryptSecretValue( - value ?? '', - isSecretValue, - ); - - if (isDefined(existingVariable)) { - entitiesToSave.push({ - id: existingVariable.id, - description: description ?? '', - isSecret: isSecretValue, - ...(shouldUpdateValue || existingVariable.isSecret !== isSecretValue - ? { value: encryptedValue } - : {}), - }); - } else { - entitiesToSave.push({ - key, - value: encryptedValue, - description: description ?? '', - isSecret: isSecretValue, - applicationId, - workspaceId, - }); - } - } - - if (entitiesToSave.length > 0) { - await this.applicationVariableRepository.save(entitiesToSave); - } - - await this.applicationVariableRepository.delete({ - applicationId, - key: Not(In(keys)), - }); - - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - 'applicationVariableMaps', - ]); - } } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type.ts index 2f193f98ad..001f652a70 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type.ts @@ -1,6 +1,5 @@ -import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type'; +import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; +import type { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -export type ApplicationVariableCacheMaps = { - byId: Partial>; - byApplicationId: Partial>; -}; +export type ApplicationVariableCacheMaps = + FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/utils/from-application-variable-entity-to-flat-application-variable.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/utils/from-application-variable-entity-to-flat-application-variable.util.ts deleted file mode 100644 index 42b3f8ab64..0000000000 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/utils/from-application-variable-entity-to-flat-application-variable.util.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; -import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type'; - -export const fromApplicationVariableEntityToFlatApplicationVariable = ( - entity: ApplicationVariableEntity, -): FlatApplicationVariable => ({ - id: entity.id, - key: entity.key, - value: entity.value, - description: entity.description, - isSecret: entity.isSecret, - applicationId: entity.applicationId, - workspaceId: entity.workspaceId, - createdAt: entity.createdAt.toISOString(), - updatedAt: entity.updatedAt.toISOString(), -}); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/workspace-application-variable-map-cache.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-variable/workspace-application-variable-map-cache.service.ts index f14d35f7eb..ab9e76ba62 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/workspace-application-variable-map-cache.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-variable/workspace-application-variable-map-cache.service.ts @@ -2,14 +2,17 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type'; -import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/utils/from-application-variable-entity-to-flat-application-variable.util'; +import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/utils/from-application-variable-entity-to-flat-application-variable.util'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @WorkspaceCache('applicationVariableMaps') @@ -17,6 +20,8 @@ export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheP constructor( @InjectRepository(ApplicationVariableEntity) private readonly applicationVariableRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, ) { super(); } @@ -24,43 +29,32 @@ export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheP async computeForCache( workspaceId: string, ): Promise { - const applicationVariableEntities = await this.applicationVariableRepository - .createQueryBuilder('applicationVariable') - .innerJoin('applicationVariable.application', 'application') - .where('application.workspaceId = :workspaceId', { workspaceId }) - .getMany(); + const [applicationVariableEntities, applications] = await Promise.all([ + this.applicationVariableRepository.find({ + where: { workspaceId }, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + ]); - const applicationVariableMaps: ApplicationVariableCacheMaps = { - byId: {}, - byApplicationId: {}, - }; + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + + const applicationVariableMaps = createEmptyFlatEntityMaps(); for (const entity of applicationVariableEntities) { const flatApplicationVariable = - fromApplicationVariableEntityToFlatApplicationVariable(entity); + fromApplicationVariableEntityToFlatApplicationVariable({ + entity, + applicationIdToUniversalIdentifierMap, + }); - applicationVariableMaps.byId[flatApplicationVariable.id] = - flatApplicationVariable; - - if (!isDefined(flatApplicationVariable.applicationId)) { - continue; - } - if ( - !isDefined( - applicationVariableMaps.byApplicationId[ - flatApplicationVariable.applicationId - ], - ) - ) { - applicationVariableMaps.byApplicationId[ - flatApplicationVariable.applicationId - ] = [flatApplicationVariable]; - continue; - } - - applicationVariableMaps.byApplicationId[ - flatApplicationVariable.applicationId - ]?.push(flatApplicationVariable); + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatApplicationVariable, + flatEntityMapsToMutate: applicationVariableMaps, + }); } return applicationVariableMaps; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts index 952f48aa46..8567d76cf7 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts @@ -19,7 +19,7 @@ import { import { ApplicationLogsService } from 'src/engine/core-modules/application-logs/application-logs.service'; import { parseApplicationLogLines } from 'src/engine/core-modules/application-logs/utils/parse-application-log-lines'; import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity'; -import type { FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type'; +import type { FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed'; @@ -205,8 +205,17 @@ export class LogicFunctionExecutorService { ); } - const flatApplicationVariables = - applicationVariableMaps.byApplicationId[flatApplication.id] ?? []; + const flatApplicationVariableUniversalIdentifiers = + applicationVariableMaps.universalIdentifiersByApplicationId[ + flatApplication.id + ] ?? []; + + const flatApplicationVariables = flatApplicationVariableUniversalIdentifiers + .map( + (universalIdentifier) => + applicationVariableMaps.byUniversalIdentifier[universalIdentifier], + ) + .filter(isDefined); return { flatApplication, flatLogicFunction, flatApplicationVariables }; } diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/__tests__/build-env-var.spec.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/__tests__/build-env-var.spec.ts index a388bc5a19..00c22fba69 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/__tests__/build-env-var.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/__tests__/build-env-var.spec.ts @@ -1,4 +1,4 @@ -import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type'; +import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var'; @@ -28,6 +28,8 @@ describe('buildEnvVar', () => { isSecret: false, applicationId: 'app-1', workspaceId: '00000000-0000-0000-0000-000000000000', + universalIdentifier: '00000000-0000-0000-0000-000000000000', + applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -39,6 +41,8 @@ describe('buildEnvVar', () => { isSecret: true, applicationId: 'app-1', workspaceId: '00000000-0000-0000-0000-000000000000', + universalIdentifier: '00000000-0000-0000-0000-000000000000', + applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -50,6 +54,8 @@ describe('buildEnvVar', () => { isSecret: false, applicationId: 'app-1', workspaceId: '00000000-0000-0000-0000-000000000000', + universalIdentifier: '00000000-0000-0000-0000-000000000000', + applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -78,6 +84,8 @@ describe('buildEnvVar', () => { isSecret: false, applicationId: 'app-1', workspaceId: '00000000-0000-0000-0000-000000000000', + universalIdentifier: '00000000-0000-0000-0000-000000000000', + applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -89,6 +97,8 @@ describe('buildEnvVar', () => { isSecret: false, applicationId: 'app-1', workspaceId: '00000000-0000-0000-0000-000000000000', + universalIdentifier: '00000000-0000-0000-0000-000000000000', + applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -112,6 +122,8 @@ describe('buildEnvVar', () => { isSecret: false, applicationId: 'app-1', workspaceId: '00000000-0000-0000-0000-000000000000', + universalIdentifier: '00000000-0000-0000-0000-000000000000', + applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var.ts index ac71ebc021..ca35e332ef 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var.ts @@ -1,4 +1,4 @@ -import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type'; +import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; export const buildEnvVar = ( diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/commands/upgrade-status.command.ts b/packages/twenty-server/src/engine/core-modules/upgrade/commands/upgrade-status.command.ts index c6780b0bbc..c1eb5be59a 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/commands/upgrade-status.command.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/commands/upgrade-status.command.ts @@ -102,6 +102,7 @@ export class UpgradeStatusCommand extends CommandRunner { ...this.formatSummary(instanceStatus, groupedWorkspaceUpgradeStatuses), ); + // oxlint-disable-next-line no-console console.log(lines.join('\n')); } catch (error) { this.logger.error( diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/constants/flat-application-variable-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/constants/flat-application-variable-editable-properties.constant.ts new file mode 100644 index 0000000000..1dba4f8750 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/constants/flat-application-variable-editable-properties.constant.ts @@ -0,0 +1,7 @@ +import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant'; + +export const FLAT_APPLICATION_VARIABLE_EDITABLE_PROPERTIES = [ + 'key', + 'description', + 'isSecret', +] as const satisfies MetadataEntityPropertyName<'applicationVariable'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/flat-application-variable.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/flat-application-variable.module.ts new file mode 100644 index 0000000000..20e0c79177 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/flat-application-variable.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { WorkspaceFlatApplicationVariableMapCacheService } from 'src/engine/metadata-modules/flat-application-variable/services/workspace-flat-application-variable-map-cache.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ApplicationEntity, ApplicationVariableEntity]), + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], + providers: [WorkspaceFlatApplicationVariableMapCacheService], + exports: [WorkspaceFlatApplicationVariableMapCacheService], +}) +export class FlatApplicationVariableModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/services/workspace-flat-application-variable-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/services/workspace-flat-application-variable-map-cache.service.ts new file mode 100644 index 0000000000..dd80a78039 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/services/workspace-flat-application-variable-map-cache.service.ts @@ -0,0 +1,62 @@ +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 { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatApplicationVariableMaps } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable-maps.type'; +import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/utils/from-application-variable-entity-to-flat-application-variable.util'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; + +@Injectable() +@WorkspaceCache('flatApplicationVariableMaps') +export class WorkspaceFlatApplicationVariableMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ApplicationVariableEntity) + private readonly applicationVariableRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const [applicationVariables, applications] = await Promise.all([ + this.applicationVariableRepository.find({ + where: { workspaceId }, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + + const flatApplicationVariableMaps = createEmptyFlatEntityMaps(); + + for (const applicationVariableEntity of applicationVariables) { + const flatApplicationVariable = + fromApplicationVariableEntityToFlatApplicationVariable({ + entity: applicationVariableEntity, + applicationIdToUniversalIdentifierMap, + }); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatApplicationVariable, + flatEntityMapsToMutate: flatApplicationVariableMaps, + }); + } + + return flatApplicationVariableMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/types/flat-application-variable-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/types/flat-application-variable-maps.type.ts new file mode 100644 index 0000000000..1055217129 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/types/flat-application-variable-maps.type.ts @@ -0,0 +1,5 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; + +export type FlatApplicationVariableMaps = + FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-variable/types/flat-application-variable.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/application/application-variable/types/flat-application-variable.type.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type.ts index 7fc67618f6..1590c7e02d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-variable/types/flat-application-variable.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type.ts @@ -1,4 +1,4 @@ -import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; +import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; export type FlatApplicationVariable = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/utils/from-application-variable-entity-to-flat-application-variable.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/utils/from-application-variable-entity-to-flat-application-variable.util.ts new file mode 100644 index 0000000000..d7777cf19a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-application-variable/utils/from-application-variable-entity-to-flat-application-variable.util.ts @@ -0,0 +1,39 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromApplicationVariableEntityToFlatApplicationVariable = ({ + entity: applicationVariableEntity, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'applicationVariable'>): FlatApplicationVariable => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + applicationVariableEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${applicationVariableEntity.applicationId} not found for applicationVariable ${applicationVariableEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return { + id: applicationVariableEntity.id, + key: applicationVariableEntity.key, + value: applicationVariableEntity.value, + description: applicationVariableEntity.description, + isSecret: applicationVariableEntity.isSecret, + workspaceId: applicationVariableEntity.workspaceId, + universalIdentifier: applicationVariableEntity.universalIdentifier, + applicationId: applicationVariableEntity.applicationId, + createdAt: applicationVariableEntity.createdAt.toISOString(), + updatedAt: applicationVariableEntity.updatedAt.toISOString(), + applicationUniversalIdentifier, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index 85e18bde45..bde4fc4da7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -20,6 +20,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "evaluationInputs", ], }, + "applicationVariable": { + "propertiesToCompare": [ + "key", + "description", + "isSecret", + ], + "propertiesToStringify": [], + }, "commandMenuItem": { "propertiesToCompare": [ "label", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index c74626b02f..e2b13ced22 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -1542,6 +1542,38 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: undefined, }, }, + applicationVariable: { + key: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + value: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + description: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + isSecret: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + createdAt: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + updatedAt: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + }, connectionProvider: { name: { toCompare: true, toStringify: false, universalProperty: undefined }, displayName: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts index 3f721add17..ab86630b37 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts @@ -275,6 +275,10 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = { workspace: null, application: null, }, + applicationVariable: { + workspace: null, + application: null, + }, viewSort: { application: null, workspace: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts index 93ae146d70..ac47c28d3a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts @@ -487,6 +487,10 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = { workspace: null, application: null, }, + applicationVariable: { + workspace: null, + application: null, + }, viewSort: { application: null, workspace: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts index f4fd588fa0..7835a1cc46 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts @@ -3,6 +3,7 @@ import { type EntityTarget, type ObjectLiteral } from 'typeorm'; import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; +import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; @@ -57,6 +58,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = { navigationMenuItem: NavigationMenuItemEntity, permissionFlag: PermissionFlagEntity, webhook: WebhookEntity, + applicationVariable: ApplicationVariableEntity, viewSort: ViewSortEntity, connectionProvider: ConnectionProviderEntity, } as const satisfies Record>; 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 a39d51c91e..da2f8ca4f8 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 @@ -112,5 +112,6 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { }, frontComponent: {}, webhook: {}, + applicationVariable: {}, connectionProvider: {}, } as const satisfies MetadataRequiredForValidation; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts index 2a2ab7c665..2e3a6cf01e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts @@ -50,6 +50,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = { viewSort: {}, frontComponent: {}, webhook: {}, + applicationVariable: {}, connectionProvider: {}, } as const satisfies MetadataSerializedRelationProperties; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts index 0d647b4942..624643f5d2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts @@ -243,6 +243,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { }, frontComponent: {}, webhook: {}, + applicationVariable: {}, viewSort: {}, connectionProvider: {}, } as const satisfies OneToManyMetadataRelationsProperties; 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 9e359cac5f..28a598f3f7 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 @@ -45,6 +45,8 @@ import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sor import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type'; import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; +import { type FlatApplicationVariableMaps } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable-maps.type'; +import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type'; import { type FlatWebhookMaps } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook-maps.type'; import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type'; import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type'; @@ -80,6 +82,7 @@ import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/works import { type UniversalFlatViewGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-group.type'; import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; +import { type UniversalFlatApplicationVariable } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type'; import { type UniversalFlatWebhook } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-webhook.type'; import { type FlatCreateAgentAction, @@ -297,6 +300,14 @@ import { type UniversalDeleteViewAction, type UniversalUpdateViewAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type'; +import { + type FlatCreateApplicationVariableAction, + type FlatDeleteApplicationVariableAction, + type FlatUpdateApplicationVariableAction, + type UniversalCreateApplicationVariableAction, + type UniversalDeleteApplicationVariableAction, + type UniversalUpdateApplicationVariableAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type'; import { type FlatCreateWebhookAction, type FlatDeleteWebhookAction, @@ -742,6 +753,22 @@ export type AllFlatEntityTypesByMetadataName = { universalFlatEntity: UniversalFlatWebhook; entity: MetadataEntity<'webhook'>; }; + applicationVariable: { + flatEntityMaps: FlatApplicationVariableMaps; + universalActions: { + create: UniversalCreateApplicationVariableAction; + update: UniversalUpdateApplicationVariableAction; + delete: UniversalDeleteApplicationVariableAction; + }; + flatActions: { + create: FlatCreateApplicationVariableAction; + update: FlatUpdateApplicationVariableAction; + delete: FlatDeleteApplicationVariableAction; + }; + flatEntity: FlatApplicationVariable; + universalFlatEntity: UniversalFlatApplicationVariable; + entity: MetadataEntity<'applicationVariable'>; + }; connectionProvider: { flatEntityMaps: FlatConnectionProviderMaps; universalActions: { 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 ecb07bfe6a..8e333567e6 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 @@ -2,6 +2,8 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`; +exports[`getMetadataRelatedMetadataNames should return related metadata names for applicationVariable 1`] = `[]`; + exports[`getMetadataRelatedMetadataNames should return related metadata names for commandMenuItem 1`] = ` [ "objectMetadata", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap index e370f70e03..1e1b6ddd3f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.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[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = ` [ @@ -18,6 +18,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with "rowLevelPermissionPredicateGroup", "viewGroup", "agent", + "applicationVariable", "connectionProvider", "frontComponent", "logicFunction", 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 d88af644a9..7a42b17f56 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 @@ -53,6 +53,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { 'flat-maps:row-level-permission-predicate-group', flatFrontComponentMaps: 'flat-maps:front-component', flatWebhookMaps: 'flat-maps:webhook', + flatApplicationVariableMaps: 'flat-maps:application-variable', flatConnectionProviderMaps: 'flat-maps:connection-provider', flatWorkspaceMemberMaps: 'flat-maps:workspace-member', apiKeyMap: 'cache:api-key-map', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts index ce3f43b173..e90bcc6f69 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts @@ -171,6 +171,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ case 'objectPermission': case 'fieldPermission': case 'webhook': + case 'applicationVariable': case 'connectionProvider': { if (!isDefined(idByUniversalIdentifier)) { return action; 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 018e1d70db..9dcf7c0543 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 @@ -41,6 +41,7 @@ import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/wo import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; +import { WorkspaceMigrationApplicationVariableActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service'; import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; @Injectable() @@ -73,6 +74,7 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService, private readonly workspaceMigrationFrontComponentActionsBuilderService: WorkspaceMigrationFrontComponentActionsBuilderService, private readonly workspaceMigrationWebhookActionsBuilderService: WorkspaceMigrationWebhookActionsBuilderService, + private readonly workspaceMigrationApplicationVariableActionsBuilderService: WorkspaceMigrationApplicationVariableActionsBuilderService, private readonly workspaceMigrationConnectionProviderActionsBuilderService: WorkspaceMigrationConnectionProviderActionsBuilderService, ) {} @@ -125,6 +127,7 @@ export class WorkspaceMigrationBuildOrchestratorService { const orchestratorActionsReport = structuredClone({ ...createEmptyOrchestratorActionsReport(), }); + const orchestratorFailureReport = structuredClone( EMPTY_ORCHESTRATOR_FAILURE_REPORT(), ); @@ -166,6 +169,7 @@ export class WorkspaceMigrationBuildOrchestratorService { flatPageLayoutTabMaps, flatFrontComponentMaps, flatWebhookMaps, + flatApplicationVariableMaps, flatConnectionProviderMaps, } = fromToAllFlatEntityMaps; @@ -826,6 +830,34 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatApplicationVariableMaps)) { + const { + from: fromFlatApplicationVariableMaps, + to: toFlatApplicationVariableMaps, + } = flatApplicationVariableMaps; + + const applicationVariableResult = + await this.workspaceMigrationApplicationVariableActionsBuilderService.validateAndBuild( + { + additionalCacheDataMaps, + from: fromFlatApplicationVariableMaps, + to: toFlatApplicationVariableMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, + workspaceId, + }, + ); + + if (applicationVariableResult.status === 'fail') { + orchestratorFailureReport.applicationVariable.push( + ...applicationVariableResult.errors, + ); + } else { + orchestratorActionsReport.applicationVariable = + applicationVariableResult.actions; + } + } + if (isDefined(flatConnectionProviderMaps)) { const { from: fromFlatConnectionProviderMaps, @@ -1031,6 +1063,11 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.webhook.update, /// + // Application Variables + ...aggregatedOrchestratorActionsReport.applicationVariable.delete, + ...aggregatedOrchestratorActionsReport.applicationVariable.create, + ...aggregatedOrchestratorActionsReport.applicationVariable.update, + // Connection providers ...aggregatedOrchestratorActionsReport.connectionProvider.delete, ...aggregatedOrchestratorActionsReport.connectionProvider.create, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap index fc0dff07d2..b4770070c2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap @@ -3,6 +3,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = ` { "agent": [], + "applicationVariable": [], "commandMenuItem": [], "connectionProvider": [], "fieldMetadata": [ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts index c30adac710..e8c1635a4c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts @@ -42,6 +42,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = { objectPermission: {}, fieldPermission: {}, frontComponent: {}, + applicationVariable: {}, connectionProvider: {}, } as const satisfies { [P in AllMetadataName]: Partial<{ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type.ts new file mode 100644 index 0000000000..c906694db3 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type.ts @@ -0,0 +1,7 @@ +import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; +import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type UniversalFlatApplicationVariable = UniversalFlatEntityFrom< + ApplicationVariableEntity, + 'applicationVariable' +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type.ts new file mode 100644 index 0000000000..8d6e9ef4da --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type.ts @@ -0,0 +1,24 @@ +import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type'; +import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type'; +import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type'; +import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type'; +import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type'; +import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type'; + +export type FlatCreateApplicationVariableAction = + BaseFlatCreateWorkspaceMigrationAction<'applicationVariable'>; + +export type UniversalCreateApplicationVariableAction = + BaseUniversalCreateWorkspaceMigrationAction<'applicationVariable'>; + +export type FlatUpdateApplicationVariableAction = + BaseFlatUpdateWorkspaceMigrationAction<'applicationVariable'>; + +export type UniversalUpdateApplicationVariableAction = + BaseUniversalUpdateWorkspaceMigrationAction<'applicationVariable'>; + +export type UniversalDeleteApplicationVariableAction = + BaseUniversalDeleteWorkspaceMigrationAction<'applicationVariable'>; + +export type FlatDeleteApplicationVariableAction = + BaseFlatDeleteWorkspaceMigrationAction<'applicationVariable'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service.ts new file mode 100644 index 0000000000..9ec36157b6 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { UniversalUpdateApplicationVariableAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-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/universal-flat-entity-update-validation-args.type'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; +import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type'; +import { FlatApplicationVariableValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service'; + +@Injectable() +export class WorkspaceMigrationApplicationVariableActionsBuilderService extends WorkspaceEntityMigrationBuilderService< + typeof ALL_METADATA_NAME.applicationVariable +> { + constructor( + private readonly flatApplicationVariableValidatorService: FlatApplicationVariableValidatorService, + ) { + super(ALL_METADATA_NAME.applicationVariable); + } + + protected validateFlatEntityCreation( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.applicationVariable + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.applicationVariable, + 'create' + > { + const validationResult = + this.flatApplicationVariableValidatorService.validateFlatApplicationVariableCreation( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatApplicationVariableToValidate } = args; + + return { + status: 'success', + action: { + type: 'create', + metadataName: 'applicationVariable', + flatEntity: flatApplicationVariableToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.applicationVariable + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.applicationVariable, + 'delete' + > { + const validationResult = + this.flatApplicationVariableValidatorService.validateFlatApplicationVariableDeletion( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatApplicationVariableToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete', + metadataName: 'applicationVariable', + universalIdentifier: + flatApplicationVariableToValidate.universalIdentifier, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.applicationVariable + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.applicationVariable, + 'update' + > { + const validationResult = + this.flatApplicationVariableValidatorService.validateFlatApplicationVariableUpdate( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { universalIdentifier, flatEntityUpdate } = args; + + const updateAction: UniversalUpdateApplicationVariableAction = { + type: 'update', + metadataName: 'applicationVariable', + universalIdentifier, + update: flatEntityUpdate, + }; + + return { + status: 'success', + action: updateAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts new file mode 100644 index 0000000000..8d28c9f8d6 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service.ts @@ -0,0 +1,127 @@ +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 { ApplicationVariableEntityExceptionCode } from 'src/engine/core-modules/application/application-variable/application-variable.exception'; +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +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/universal-flat-entity-update-validation-args.type'; +import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; + +@Injectable() +export class FlatApplicationVariableValidatorService { + public validateFlatApplicationVariableCreation({ + flatEntityToValidate: flatApplicationVariable, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatApplicationVariableMaps: optimisticFlatApplicationVariableMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.applicationVariable + >): FailedFlatEntityValidation<'applicationVariable', 'create'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier: flatApplicationVariable.universalIdentifier, + key: flatApplicationVariable.key, + }, + metadataName: 'applicationVariable', + type: 'create', + }); + + if (!isNonEmptyString(flatApplicationVariable.key)) { + validationResult.errors.push({ + code: ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT, + message: t`Application variable key is required`, + userFriendlyMessage: msg`Application variable key is required`, + }); + } + + const existingVariableWithSameKey = Object.values( + optimisticFlatApplicationVariableMaps.byUniversalIdentifier, + ).find( + (variable) => + isDefined(variable) && + variable.key === flatApplicationVariable.key && + variable.universalIdentifier !== + flatApplicationVariable.universalIdentifier, + ); + + if (isDefined(existingVariableWithSameKey)) { + validationResult.errors.push({ + code: ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT, + message: t`Application variable key must be unique`, + userFriendlyMessage: msg`Application variable key must be unique`, + }); + } + + return validationResult; + } + + public validateFlatApplicationVariableDeletion({ + flatEntityToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatApplicationVariableMaps: optimisticFlatApplicationVariableMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.applicationVariable + >): FailedFlatEntityValidation<'applicationVariable', 'delete'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier: flatEntityToValidate.universalIdentifier, + key: flatEntityToValidate.key, + }, + metadataName: 'applicationVariable', + type: 'delete', + }); + + const existingVariable = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatEntityToValidate.universalIdentifier, + flatEntityMaps: optimisticFlatApplicationVariableMaps, + }); + + if (!isDefined(existingVariable)) { + validationResult.errors.push({ + code: ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND, + message: t`Application variable not found`, + userFriendlyMessage: msg`Application variable not found`, + }); + } + + return validationResult; + } + + public validateFlatApplicationVariableUpdate({ + universalIdentifier, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatApplicationVariableMaps: optimisticFlatApplicationVariableMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.applicationVariable + >): FailedFlatEntityValidation<'applicationVariable', 'update'> { + const fromFlatApplicationVariable = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatApplicationVariableMaps, + }); + + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'applicationVariable', + type: 'update', + }); + + if (!isDefined(fromFlatApplicationVariable)) { + validationResult.errors.push({ + code: ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND, + message: t`Application variable not found`, + userFriendlyMessage: msg`Application variable not found`, + }); + } + + 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 f44b3e6995..d32733d856 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 @@ -29,6 +29,7 @@ import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/wor import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-group-validator.service'; import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service'; import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service'; +import { FlatApplicationVariableValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service'; import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service'; import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-connection-provider-validator.service'; @@ -64,6 +65,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man FlatRowLevelPermissionPredicateGroupValidatorService, FlatFrontComponentValidatorService, FlatWebhookValidatorService, + FlatApplicationVariableValidatorService, FlatConnectionProviderValidatorService, ], exports: [ @@ -95,6 +97,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man FlatRowLevelPermissionPredicateGroupValidatorService, FlatFrontComponentValidatorService, FlatWebhookValidatorService, + FlatApplicationVariableValidatorService, FlatConnectionProviderValidatorService, ], }) 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 e4e9159862..e5c52274ff 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 @@ -28,6 +28,7 @@ import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/wo import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; +import { WorkspaceMigrationApplicationVariableActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service'; import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; import { WorkspaceMigrationConnectionProviderActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/workspace-migration-connection-provider-actions-builder.service'; import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module'; @@ -63,6 +64,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService, WorkspaceMigrationFrontComponentActionsBuilderService, WorkspaceMigrationWebhookActionsBuilderService, + WorkspaceMigrationApplicationVariableActionsBuilderService, WorkspaceMigrationConnectionProviderActionsBuilderService, ], exports: [ @@ -94,6 +96,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- FlatFieldMetadataTypeValidatorService, WorkspaceMigrationFrontComponentActionsBuilderService, WorkspaceMigrationWebhookActionsBuilderService, + WorkspaceMigrationApplicationVariableActionsBuilderService, WorkspaceMigrationConnectionProviderActionsBuilderService, ], }) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/create-application-variable-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/create-application-variable-action-handler.service.ts new file mode 100644 index 0000000000..201027c254 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/create-application-variable-action-handler.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; + +import { v4 } from 'uuid'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util'; +import { + FlatCreateApplicationVariableAction, + UniversalCreateApplicationVariableAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreateApplicationVariableActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create', + 'applicationVariable', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction({ + action, + flatApplication, + workspaceId, + }: WorkspaceMigrationActionRunnerArgs): Promise { + const emptyUniversalForeignKeyAggregators = + getUniversalFlatEntityEmptyForeignKeyAggregators({ + metadataName: 'applicationVariable', + }); + + return { + ...action, + flatEntity: { + ...action.flatEntity, + applicationId: flatApplication.id, + id: action.id ?? v4(), + workspaceId, + ...emptyUniversalForeignKeyAggregators, + }, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner } = context; + const { flatEntity } = flatAction; + + await this.insertFlatEntitiesInRepository({ + queryRunner, + flatEntities: [flatEntity], + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/delete-application-variable-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/delete-application-variable-action-handler.service.ts new file mode 100644 index 0000000000..3a14959bb1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/delete-application-variable-action-handler.service.ts @@ -0,0 +1,51 @@ +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 { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; +import { + FlatDeleteApplicationVariableAction, + UniversalDeleteApplicationVariableAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteApplicationVariableActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete', + 'applicationVariable', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return this.transpileUniversalDeleteActionToFlatDeleteAction(context); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + + const applicationVariableRepository = + queryRunner.manager.getRepository( + ApplicationVariableEntity, + ); + + await applicationVariableRepository.delete({ + id: flatAction.entityId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/update-application-variable-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/update-application-variable-action-handler.service.ts new file mode 100644 index 0000000000..2c5bfed856 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/update-application-variable-action-handler.service.ts @@ -0,0 +1,98 @@ +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 { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util'; +import { + FlatUpdateApplicationVariableAction, + UniversalUpdateApplicationVariableAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class UpdateApplicationVariableActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update', + 'applicationVariable', +) { + constructor( + private readonly secretEncryptionService: SecretEncryptionService, + ) { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, allFlatEntityMaps } = context; + + const flatApplicationVariable = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: allFlatEntityMaps.flatApplicationVariableMaps, + universalIdentifier: action.universalIdentifier, + }); + + const update = resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'applicationVariable', + universalUpdate: action.update, + allFlatEntityMaps, + }); + + return { + type: 'update', + metadataName: 'applicationVariable', + entityId: flatApplicationVariable.id, + update, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + const { entityId, update } = flatAction; + const applicationVariableRepository = + queryRunner.manager.getRepository( + ApplicationVariableEntity, + ); + + const existing = await applicationVariableRepository.findOne({ + where: { id: entityId, workspaceId }, + }); + + if ( + update.isSecret !== undefined && + update.isSecret && + existing && + !existing.isSecret + ) { + (update as Record).value = + this.secretEncryptionService.encrypt(existing.value); + } + + if ( + update.isSecret !== undefined && + !update.isSecret && + existing && + existing.isSecret + ) { + (update as Record).value = + this.secretEncryptionService.decrypt(existing.value); + } + + await applicationVariableRepository.update( + { id: entityId, workspaceId }, + update, + ); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/create-object-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/create-object-action-handler.service.ts index 4c1e3e6c80..e547632a80 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/create-object-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/create-object-action-handler.service.ts @@ -15,8 +15,8 @@ import { import { fromUniversalFlatFieldMetadataToFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util'; import { fromUniversalFlatObjectMetadataToFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util'; import { - WorkspaceMigrationActionRunnerContext, type WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { flatEntityToScalarFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/flat-entity-to-scalar-flat-entity.util'; import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util'; @@ -41,7 +41,7 @@ export class CreateObjectActionHandlerService extends WorkspaceMigrationRunnerAc override async transpileUniversalActionToFlatAction( context: WorkspaceMigrationActionRunnerArgs, ): Promise { - const { action, workspaceId, allFlatEntityMaps } = context; + const { action, allFlatEntityMaps } = context; const { fieldIdByUniversalIdentifier, id: providedObjectId } = action; const allFieldIdToBeCreatedInActionByUniversalIdentifierMap = new Map< 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 00d75dbd06..e5445069a5 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 @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service'; import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/delete-agent-action-handler.service'; @@ -81,6 +82,9 @@ import { UpdateViewSortActionHandlerService } from 'src/engine/workspace-manager import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service'; import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service'; import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service'; +import { CreateApplicationVariableActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/create-application-variable-action-handler.service'; +import { DeleteApplicationVariableActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/delete-application-variable-action-handler.service'; +import { UpdateApplicationVariableActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/update-application-variable-action-handler.service'; import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service'; import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service'; import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service'; @@ -92,6 +96,7 @@ import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspa imports: [ TypeOrmModule.forFeature([ApplicationEntity]), WorkspaceSchemaManagerModule, + SecretEncryptionModule, ], providers: [ CreateFieldActionHandlerService, @@ -201,6 +206,11 @@ import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspa CreateWebhookActionHandlerService, UpdateWebhookActionHandlerService, DeleteWebhookActionHandlerService, + + CreateApplicationVariableActionHandlerService, + UpdateApplicationVariableActionHandlerService, + DeleteApplicationVariableActionHandlerService, + CreateConnectionProviderActionHandlerService, UpdateConnectionProviderActionHandlerService, DeleteConnectionProviderActionHandlerService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts index d97d711776..2f6a456cf9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts @@ -28,5 +28,6 @@ export const METADATA_EVENTS_TO_EMIT = { logicFunction: true, viewSort: true, webhook: false, + applicationVariable: false, connectionProvider: true, } as const satisfies { [P in AllMetadataName]: boolean }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts index 0bedc7e398..013bba6b7e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts @@ -69,6 +69,9 @@ const deriveAllMetadataEventsFromCreateAction = ( return [objectEvent, ...fieldEvents]; } + case 'applicationVariable': { + return []; + } case 'view': case 'viewField': case 'viewGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts index d8df90b5f3..7de10f3907 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts @@ -31,6 +31,9 @@ const deriveAllMetadataEventsFromDeleteAction = ({ allFlatEntityMaps, }: DeriveMetadataEventsFromDeleteActionArgs): MetadataEvent[] => { switch (flatAction.metadataName) { + case 'applicationVariable': { + return []; + } case 'fieldMetadata': case 'objectMetadata': case 'view': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts index cb89768e23..5faaa51112 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts @@ -110,6 +110,9 @@ const deriveAllMetadataEventsFromUpdateAction = ({ return [deleteIndexMetadataEvent, createIndexMetadataEvent]; } + case 'applicationVariable': { + return []; + } case 'fieldMetadata': case 'objectMetadata': case 'view': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/flat-entity-to-scalar-flat-entity.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/flat-entity-to-scalar-flat-entity.util.ts index a5429f779c..34bd2aa0dc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/flat-entity-to-scalar-flat-entity.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/flat-entity-to-scalar-flat-entity.util.ts @@ -26,5 +26,9 @@ export const flatEntityToScalarFlatEntity = ({ result.applicationId = flatEntityRecord.applicationId; result.universalIdentifier = flatEntityRecord.universalIdentifier; + if (metadataName === 'applicationVariable' && result.isSecret === true) { + result.value = '********'; + } + return result as ScalarFlatEntity>; }; 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 5d3f42b50e..8d313ce09c 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 @@ -72,6 +72,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({ case 'objectPermission': case 'fieldPermission': case 'webhook': + case 'applicationVariable': case 'connectionProvider': { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({ flatEntity: flatAction.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 f0530d93ae..d262a44c7a 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 @@ -44,6 +44,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({ case 'objectPermission': case 'fieldPermission': case 'webhook': + case 'applicationVariable': case 'connectionProvider': { 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 e48497a5c0..e5a94ff070 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 @@ -65,6 +65,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({ case 'objectPermission': case 'fieldPermission': case 'webhook': + case 'applicationVariable': case 'connectionProvider': { const flatEntityMapsKey = getMetadataFlatEntityMapsKey( flatAction.metadataName, diff --git a/packages/twenty-shared/src/application/applicationVariablesType.ts b/packages/twenty-shared/src/application/applicationVariablesType.ts index 4e205ee73f..76a35fd706 100644 --- a/packages/twenty-shared/src/application/applicationVariablesType.ts +++ b/packages/twenty-shared/src/application/applicationVariablesType.ts @@ -1,9 +1,18 @@ import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; -type ApplicationVariable = SyncableEntityOptions & { - value?: string; +type SecretApplicationVariable = SyncableEntityOptions & { description?: string; - isSecret?: boolean; + isSecret: true; }; +type NonSecretApplicationVariable = SyncableEntityOptions & { + value?: string; + description?: string; + isSecret?: false; +}; + +export type ApplicationVariable = + | SecretApplicationVariable + | NonSecretApplicationVariable; + export type ApplicationVariables = Record; diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index 027824cb60..37d71d251d 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -10,7 +10,10 @@ export type { AgentManifest } from './agentManifestType'; export type { AppConnection } from './appConnectionType'; export type { ApplicationManifest } from './applicationType'; -export type { ApplicationVariables } from './applicationVariablesType'; +export type { + ApplicationVariable, + ApplicationVariables, +} from './applicationVariablesType'; export type { AssetManifest } from './assetManifestType'; export type { ConnectionProviderManifest } from './connectionProviderManifestType'; export type { ConnectionProviderType } from './connectionProviderType'; diff --git a/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts index d7a34ddc51..feed9014cc 100644 --- a/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts +++ b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts @@ -26,5 +26,6 @@ export const ALL_METADATA_NAME = { fieldPermission: 'fieldPermission', frontComponent: 'frontComponent', webhook: 'webhook', + applicationVariable: 'applicationVariable', connectionProvider: 'connectionProvider', } as const;