From 584389e17fbe859d493162aa1952629e53653cac Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 26 Sep 2025 10:24:36 +0200 Subject: [PATCH] add code checksum to flat serverless function (#14721) ## Context Now adding serverless code sync within the migration v2 logic itself. To do that we need to follow the - Prepare flat input - Build migration - Run migration steps where now the serverless function entity will store in DB and cache a checksum of its code and the flat input will contain the code with the checksum. Build will compare checksum and create an update action containing the code if it has changed and the migration will now run the corresponding services (instead of calling those in the parent serverless service exposed in the API) allowing us to keep that logic functional for other use cases such as import/export, twenty-cli and twenty upgrades. --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + packages/twenty-server/project.json | 7 ++ ...2648930-addChecksumToServerlessFunction.ts | 19 ++++ .../interfaces/storage-driver.interface.ts | 4 +- .../file-storage/drivers/local.driver.ts | 8 +- .../file-storage/drivers/s3.driver.ts | 63 ++++++++++- .../file-storage/file-storage.service.ts | 4 +- .../file/interfaces/file-folder.interface.ts | 4 + .../utils/serverless-get-folder.utils.ts | 6 +- ...s-function-editable-properties.constant.ts | 2 + ...function-properties-to-compare.constant.ts | 1 + .../serverless-function.entity.ts | 3 + .../serverless-function-v2.service.ts | 86 ++------------ .../types/flat-serverless-function.type.ts | 1 + ...-input-to-flat-serverless-function.util.ts | 1 + ...erless-function-to-update-or-throw.util.ts | 8 +- ...ess-function-create-code-checksum.utils.ts | 17 +++ ...erless-function-actions-builder.service.ts | 7 +- ...tion-serverless-function-action-v2.type.ts | 16 +-- ...verless-function-action-handler.service.ts | 58 +++++----- ...verless-function-action-handler.service.ts | 79 ++++++++----- ...verless-function-action-handler.service.ts | 105 +++++++++++------- ...runner-action-handler-service.interface.ts | 37 +++++- ...-runner-action-handler-registry.service.ts | 21 +++- .../workspace-migration-runner-v2.service.ts | 30 ++++- 26 files changed, 374 insertions(+), 215 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1758802648930-addChecksumToServerlessFunction.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-code-checksum.utils.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 9d8f6c6b29..f5e0420646 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1206,6 +1206,7 @@ export enum FileFolder { PersonPicture = 'PersonPicture', ProfilePicture = 'ProfilePicture', ServerlessFunction = 'ServerlessFunction', + ServerlessFunctionToDelete = 'ServerlessFunctionToDelete', WorkspaceLogo = 'WorkspaceLogo' } diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index cd6303e625..a23ae1ff0e 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1170,6 +1170,7 @@ export enum FileFolder { PersonPicture = 'PersonPicture', ProfilePicture = 'ProfilePicture', ServerlessFunction = 'ServerlessFunction', + ServerlessFunctionToDelete = 'ServerlessFunctionToDelete', WorkspaceLogo = 'WorkspaceLogo' } diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index 904552835f..774d496e38 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -161,6 +161,13 @@ ] } }, + "database:migrate:generate": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-server", + "command": "npx nx typeorm -- migration:generate src/database/typeorm/core/migrations/common/{args.migrationName} -d src/database/typeorm/core/core.datasource.ts" + } + }, "database:migrate:revert": { "executor": "nx:run-commands", "dependsOn": ["build"], diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1758802648930-addChecksumToServerlessFunction.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1758802648930-addChecksumToServerlessFunction.ts new file mode 100644 index 0000000000..3210296abd --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1758802648930-addChecksumToServerlessFunction.ts @@ -0,0 +1,19 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddChecksumToServerlessFunction1758802648930 + implements MigrationInterface +{ + name = 'AddChecksumToServerlessFunction1758802648930'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" ADD "checksum" text`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" DROP COLUMN "checksum"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts index 86628b8d60..09ee385588 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts @@ -10,8 +10,8 @@ export interface StorageDriver { mimeType: string | undefined; }): Promise; move(params: { - from: { folderPath: string; filename: string }; - to: { folderPath: string; filename: string }; + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; }): Promise; copy(params: { from: { folderPath: string; filename?: string }; diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts index b0c0e86132..d665577cbf 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts @@ -87,19 +87,19 @@ export class LocalDriver implements StorageDriver { } async move(params: { - from: { folderPath: string; filename: string }; - to: { folderPath: string; filename: string }; + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; }): Promise { const fromPath = join( `${this.options.storagePath}/`, params.from.folderPath, - params.from.filename, + params.from.filename || '', ); const toPath = join( `${this.options.storagePath}/`, params.to.folderPath, - params.to.filename, + params.to.filename || '', ); await this.createFolder(dirname(toPath)); diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts index 5c726681b9..2d6a391c32 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts @@ -70,10 +70,7 @@ export class S3Driver implements StorageDriver { await this.s3Client.send(command); } - // @ts-expect-error legacy noImplicitAny - private async emptyS3Directory(folderPath) { - this.logger.log(`${folderPath} - emptying folder`); - + private async fetchS3FolderContents(folderPath: string) { const listParams = { Bucket: this.bucketName, Prefix: folderPath, @@ -82,6 +79,15 @@ export class S3Driver implements StorageDriver { const listObjectsCommand = new ListObjectsV2Command(listParams); const listedObjects = await this.s3Client.send(listObjectsCommand); + return listedObjects; + } + + // @ts-expect-error legacy noImplicitAny + private async emptyS3Directory(folderPath) { + this.logger.log(`${folderPath} - emptying folder`); + + const listedObjects = await this.fetchS3FolderContents(folderPath); + this.logger.log( `${folderPath} - listed objects`, listedObjects.Contents, @@ -172,9 +178,15 @@ export class S3Driver implements StorageDriver { } async move(params: { - from: { folderPath: string; filename: string }; - to: { folderPath: string; filename: string }; + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; }): Promise { + if (!params.from.filename || !params.to.filename) { + await this.moveS3Folder(params); + + return; + } + const fromKey = `${params.from.folderPath}/${params.from.filename}`; const toKey = `${params.to.folderPath}/${params.to.filename}`; @@ -215,6 +227,45 @@ export class S3Driver implements StorageDriver { } } + async moveS3Folder(params: { + from: { folderPath: string }; + to: { folderPath: string }; + }): Promise { + const fromKey = `${params.from.folderPath}`; + + const listedObjects = await this.fetchS3FolderContents(fromKey); + + if (!listedObjects.Contents || listedObjects.Contents.length === 0) { + throw new Error( + `No objects found in the source folder ${params.from.folderPath}.`, + ); + } + + for (const object of listedObjects.Contents) { + const folderAndFilePaths = this.extractFolderAndFilePaths(object.Key); + + if (!isDefined(folderAndFilePaths)) { + continue; + } + + const { fromFolderPath, filename } = folderAndFilePaths; + + const toFolderPath = fromFolderPath.replace( + params.from.folderPath, + params.to.folderPath, + ); + + if (!isDefined(toFolderPath)) { + continue; + } + + await this.move({ + from: { folderPath: fromFolderPath, filename }, + to: { folderPath: toFolderPath, filename }, + }); + } + } + extractFolderAndFilePaths(objectKey: string | undefined) { if (!isDefined(objectKey)) { return; diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts index a9dee1b863..0a354dabd4 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts @@ -36,8 +36,8 @@ export class FileStorageService implements StorageDriver { } move(params: { - from: { folderPath: string; filename: string }; - to: { folderPath: string; filename: string }; + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; }): Promise { const driver = this.fileStorageDriverFactory.getCurrentDriver(); diff --git a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts index db7ad92c02..b9807f284d 100644 --- a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts @@ -8,6 +8,7 @@ export enum FileFolder { Attachment = 'attachment', PersonPicture = 'person-picture', ServerlessFunction = 'serverless-function', + ServerlessFunctionToDelete = 'serverless-function-to-delete', File = 'file', } @@ -35,6 +36,9 @@ export const fileFolderConfigs: Record = { [FileFolder.ServerlessFunction]: { ignoreExpirationToken: false, }, + [FileFolder.ServerlessFunctionToDelete]: { + ignoreExpirationToken: false, + }, [FileFolder.File]: { ignoreExpirationToken: false, }, diff --git a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts index 3915a96295..9e42a2cff3 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts @@ -14,9 +14,11 @@ import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverl export const getServerlessFolder = ({ serverlessFunction, version, + toDelete = false, }: { serverlessFunction: ServerlessFunctionEntity | FlatServerlessFunction; version?: 'draft' | 'latest' | (string & NonNullable); + toDelete?: boolean; }) => { if (version === 'latest' && !isDefined(serverlessFunction.latestVersion)) { throw new ServerlessFunctionException( @@ -30,7 +32,9 @@ export const getServerlessFolder = ({ return join( 'workspace-' + serverlessFunction.workspaceId, - FileFolder.ServerlessFunction, + toDelete + ? FileFolder.ServerlessFunctionToDelete + : FileFolder.ServerlessFunction, serverlessFunction.id, computedVersion || '', ); diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts index 21110f6bd1..8e8512efb3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts @@ -4,4 +4,6 @@ export const FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES = [ 'name', 'description', 'timeoutSeconds', + 'checksum', + 'code', ] as const satisfies (keyof FlatServerlessFunction)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-properties-to-compare.constant.ts index 7304492653..6a6c8c2634 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-properties-to-compare.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-properties-to-compare.constant.ts @@ -3,4 +3,5 @@ import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverl export const FLAT_SERVERLESS_FUNCTION_PROPERTIES_TO_COMPARE = [ ...FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES, + 'deletedAt', ] as const satisfies (keyof FlatServerlessFunction)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts index 472d4b83c7..b5b150aa7f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -71,6 +71,9 @@ export class ServerlessFunctionEntity @Column({ nullable: true, type: 'uuid' }) applicationId: string | null; + @Column({ nullable: true, type: 'text' }) + checksum: string | null; + @OneToMany( () => CronTrigger, (cronTrigger) => cronTrigger.serverlessFunction, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts index f10d3faeba..7e7a56873f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { basename, dirname, join } from 'path'; - import { isDefined } from 'twenty-shared/utils'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.'; @@ -10,14 +8,9 @@ import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modul import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util'; import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; -import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; -import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionException, ServerlessFunctionExceptionCode, @@ -33,8 +26,6 @@ export class ServerlessFunctionV2Service { constructor( private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, - private readonly fileStorageService: FileStorageService, - private readonly serverlessService: ServerlessService, ) {} async createOne( @@ -87,7 +78,9 @@ export class ServerlessFunctionV2Service { ); } - const updatedFlatEntityMaps = + const { + flatServerlessFunctionMaps: recomputedExistingFlatServerlessFunctionMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, @@ -95,33 +88,10 @@ export class ServerlessFunctionV2Service { }, ); - const createdServerlessFunction = - updatedFlatEntityMaps.flatServerlessFunctionMaps.byId[ - flatServerlessFunctionToCreate.id - ]; - - if (!isDefined(createdServerlessFunction)) { - throw new ServerlessFunctionException( - 'Created serverless function not found in recomputed cache', - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, - ); - } - - const draftFileFolder = getServerlessFolder({ - serverlessFunction: createdServerlessFunction, - version: 'draft', + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatServerlessFunctionToCreate.id, + flatEntityMaps: recomputedExistingFlatServerlessFunctionMaps, }); - - for (const file of await getBaseTypescriptProjectFiles) { - await this.fileStorageService.write({ - file: file.content, - name: file.name, - mimeType: undefined, - folder: join(draftFileFolder, file.path), - }); - } - - return createdServerlessFunction; } async updateOne( @@ -179,7 +149,9 @@ export class ServerlessFunctionV2Service { ); } - const updatedFlatEntityMaps = + const { + flatServerlessFunctionMaps: recomputedExistingFlatServerlessFunctionMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, @@ -187,34 +159,10 @@ export class ServerlessFunctionV2Service { }, ); - const updatedFlatServerlessFunction = - updatedFlatEntityMaps.flatServerlessFunctionMaps.byId[ - optimisticallyUpdatedFlatServerlessFunction.id - ]; - - if (!isDefined(updatedFlatServerlessFunction)) { - throw new ServerlessFunctionException( - 'Updated serverless function not found in recomputed cache', - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, - ); - } - - const fileFolder = getServerlessFolder({ - serverlessFunction: updatedFlatServerlessFunction, - version: 'draft', + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: optimisticallyUpdatedFlatServerlessFunction.id, + flatEntityMaps: recomputedExistingFlatServerlessFunctionMaps, }); - - for (const key of Object.keys(serverlessFunctionInput.code)) { - await this.fileStorageService.write({ - // @ts-expect-error legacy noImplicitAny - file: serverlessFunctionInput.code[key], - name: basename(key), - mimeType: undefined, - folder: join(fileFolder, dirname(key)), - }); - } - - return updatedFlatServerlessFunction; } async deleteOne({ @@ -287,10 +235,6 @@ export class ServerlessFunctionV2Service { }, ); - this.serverlessService.delete( - existingFlatServerlessFunction as ServerlessFunctionEntity, - ); - return findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId: optimisticallyUpdatedFlatServerlessFunctionWithDeletedAt.id, flatEntityMaps: recomputedExistingFlatServerlessFunctionMaps, @@ -358,12 +302,6 @@ export class ServerlessFunctionV2Service { ); } - this.fileStorageService.delete({ - folderPath: getServerlessFolder({ - serverlessFunction: existingFlatServerlessFunction, - }), - }); - return existingFlatServerlessFunction; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type.ts index 43668d3b7a..18e2924eff 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type.ts @@ -16,4 +16,5 @@ export type FlatServerlessFunction = Omit< ServerlessFunctionEntityRelationProperties > & { universalIdentifier: string; + code?: JSON; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts index 112e04303b..d2cdb899af 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts @@ -32,5 +32,6 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({ timeoutSeconds: createServerlessFunctionInput.timeoutSeconds ?? 300, layerVersion: LAST_LAYER_VERSION, workspaceId, + checksum: null, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts index cbd8438a90..04d07e6912 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts @@ -13,6 +13,7 @@ import { ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; +import { serverlessFunctionCreateCodeChecksum } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-code-checksum.utils'; import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow = @@ -39,7 +40,12 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr ); } const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( - rawUpdateServerlessFunctionInput, + { + ...rawUpdateServerlessFunctionInput, + checksum: serverlessFunctionCreateCodeChecksum( + rawUpdateServerlessFunctionInput.code, + ), + }, FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-code-checksum.utils.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-code-checksum.utils.ts new file mode 100644 index 0000000000..64559d823f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-code-checksum.utils.ts @@ -0,0 +1,17 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; + +export const serverlessFunctionCreateCodeChecksum = (code: JSON): string => { + if (!isDefined(code) || typeof code !== 'object') { + return serverlessFunctionCreateHash(''); + } + + const codeObj = code as unknown as Record; + const sortedKeys = Object.keys(codeObj).sort(); + const concatenatedContent = sortedKeys + .map((key) => `${key}:${codeObj[key]}`) + .join('|'); + + return serverlessFunctionCreateHash(concatenatedContent); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service.ts index 8d85165af1..5e423087bb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service.ts @@ -45,9 +45,9 @@ export class WorkspaceMigrationV2ServerlessFunctionActionsBuilderService extends const validationResult = await this.flatServerlessFunctionValidatorService.validateFlatServerlessFunctionCreation( { - dependencyOptimisticFlatEntityMaps, flatServerlessFunctionToValidate, optimisticFlatServerlessFunctionMaps, + dependencyOptimisticFlatEntityMaps, }, ); @@ -83,9 +83,9 @@ export class WorkspaceMigrationV2ServerlessFunctionActionsBuilderService extends const validationResult = this.flatServerlessFunctionValidatorService.validateFlatServerlessFunctionDeletion( { - dependencyOptimisticFlatEntityMaps, flatServerlessFunctionToValidate, optimisticFlatServerlessFunctionMaps, + dependencyOptimisticFlatEntityMaps, }, ); @@ -135,9 +135,9 @@ export class WorkspaceMigrationV2ServerlessFunctionActionsBuilderService extends const validationResult = this.flatServerlessFunctionValidatorService.validateFlatServerlessFunctionUpdate( { - dependencyOptimisticFlatEntityMaps, flatServerlessFunctionToValidate: toFlatServerlessFunction, optimisticFlatServerlessFunctionMaps, + dependencyOptimisticFlatEntityMaps, }, ); @@ -152,6 +152,7 @@ export class WorkspaceMigrationV2ServerlessFunctionActionsBuilderService extends type: 'update_serverless_function', serverlessFunctionId: toFlatServerlessFunction.id, updates: serverlessFunctionUpdatedProperties, + code: toFlatServerlessFunction.code, }; return { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type.ts index ebeee27f92..4d82c78233 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type.ts @@ -1,14 +1,6 @@ -import { type FromTo } from 'twenty-shared/types'; - -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { type FlatServerlessFunctionPropertiesToCompare } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function-properties-to-compare.type'; import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; - -export type FlatServerlessFunctionPropertyUpdate< - P extends FlatServerlessFunctionPropertiesToCompare, -> = { - property: P; -} & FromTo; +import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type'; export type CreateServerlessFunctionAction = { type: 'create_serverless_function'; @@ -18,9 +10,13 @@ export type CreateServerlessFunctionAction = { export type UpdateServerlessFunctionAction = { type: 'update_serverless_function'; serverlessFunctionId: string; + code?: JSON; updates: Array< { - [P in FlatServerlessFunctionPropertiesToCompare]: FlatServerlessFunctionPropertyUpdate

; + [P in FlatServerlessFunctionPropertiesToCompare]: PropertyUpdate< + FlatServerlessFunction, + P + >; }[FlatServerlessFunctionPropertiesToCompare] >; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts index 3a64add6fd..90321aea00 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { - OptimisticallyApplyActionOnAllFlatEntityMapsArgs, - WorkspaceMigrationRunnerActionHandler, -} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { join } from 'path'; -import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; +import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { CreateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; @@ -15,28 +15,10 @@ import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( 'create_serverless_function', ) { - constructor() { + constructor(private readonly fileStorageService: FileStorageService) { super(); } - optimisticallyApplyActionOnAllFlatEntityMaps({ - action, - allFlatEntityMaps, - }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { - const { flatServerlessFunctionMaps } = allFlatEntityMaps; - const { serverlessFunction } = action; - - const updatedFlatServerlessFunctionMaps = - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: serverlessFunction, - flatEntityMaps: flatServerlessFunctionMaps, - }); - - return { - flatServerlessFunctionMaps: updatedFlatServerlessFunctionMaps, - }; - } - async executeForMetadata( context: WorkspaceMigrationActionRunnerArgs, ): Promise { @@ -52,11 +34,31 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra ...serverlessFunction, workspaceId, }); + + const draftFileFolder = getServerlessFolder({ + serverlessFunction, + version: 'draft', + }); + + for (const file of await getBaseTypescriptProjectFiles) { + await this.fileStorageService.write({ + file: file.content, + name: file.name, + mimeType: undefined, + folder: join(draftFileFolder, file.path), + }); + } } - async executeForWorkspaceSchema( - _context: WorkspaceMigrationActionRunnerArgs, + async rollbackForMetadata( + context: WorkspaceMigrationActionRunnerArgs, ): Promise { - return; + const { action } = context; + + await this.fileStorageService.delete({ + folderPath: getServerlessFolder({ + serverlessFunction: action.serverlessFunction, + }), + }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts index 806a6d7d30..87e91b82fb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { - OptimisticallyApplyActionOnAllFlatEntityMapsArgs, - WorkspaceMigrationRunnerActionHandler, -} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; -import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { DeleteServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; @@ -15,28 +13,10 @@ import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( 'delete_serverless_function', ) { - constructor() { + constructor(private readonly fileStorageService: FileStorageService) { super(); } - optimisticallyApplyActionOnAllFlatEntityMaps({ - action, - allFlatEntityMaps, - }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { - const { flatServerlessFunctionMaps } = allFlatEntityMaps; - const { serverlessFunctionId } = action; - - const updatedFlatServerlessFunctionMaps = - deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: serverlessFunctionId, - flatEntityMaps: flatServerlessFunctionMaps, - }); - - return { - flatServerlessFunctionMaps: updatedFlatServerlessFunctionMaps, - }; - } - async executeForMetadata( context: WorkspaceMigrationActionRunnerArgs, ): Promise { @@ -52,11 +32,54 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra id: serverlessFunctionId, workspaceId, }); + + const existingServerlessFunction = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: serverlessFunctionId, + flatEntityMaps: context.allFlatEntityMaps.flatServerlessFunctionMaps, + }); + + // TODO: Should implement a cron task or a job to delete the files after a certain period of time + await this.fileStorageService.move({ + from: { + folderPath: getServerlessFolder({ + serverlessFunction: existingServerlessFunction, + }), + }, + to: { + folderPath: getServerlessFolder({ + serverlessFunction: existingServerlessFunction, + toDelete: true, + }), + }, + }); } - async executeForWorkspaceSchema( - _context: WorkspaceMigrationActionRunnerArgs, + async rollbackForMetadata( + context: WorkspaceMigrationActionRunnerArgs, ): Promise { - return; + const { action } = context; + const { serverlessFunctionId } = action; + + const existingServerlessFunction = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: serverlessFunctionId, + flatEntityMaps: context.allFlatEntityMaps.flatServerlessFunctionMaps, + }); + + await this.fileStorageService.move({ + from: { + folderPath: getServerlessFolder({ + serverlessFunction: existingServerlessFunction, + toDelete: true, + }), + }, + to: { + folderPath: getServerlessFolder({ + serverlessFunction: existingServerlessFunction, + toDelete: false, + }), + }, + }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts index 352187258e..41c947a046 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts @@ -1,14 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { - OptimisticallyApplyActionOnAllFlatEntityMapsArgs, - WorkspaceMigrationRunnerActionHandler, -} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { basename, dirname, join } from 'path'; + +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; -import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; +import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; import { UpdateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util'; @@ -17,44 +20,18 @@ import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/wo export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( 'update_serverless_function', ) { - constructor() { + constructor( + private readonly fileStorageService: FileStorageService, + private readonly serverlessService: ServerlessService, + ) { super(); } - optimisticallyApplyActionOnAllFlatEntityMaps({ - action, - allFlatEntityMaps, - }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { - const { flatServerlessFunctionMaps } = allFlatEntityMaps; - const { serverlessFunctionId } = action; - - const existingServerlessFunction = - findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: serverlessFunctionId, - flatEntityMaps: flatServerlessFunctionMaps, - }); - - const updatedServerlessFunction = { - ...existingServerlessFunction, - ...fromWorkspaceMigrationUpdateActionToPartialEntity(action), - }; - - const updatedFlatServerlessFunctionMaps = - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: updatedServerlessFunction, - flatEntityMaps: flatServerlessFunctionMaps, - }); - - return { - flatServerlessFunctionMaps: updatedFlatServerlessFunctionMaps, - }; - } - async executeForMetadata( context: WorkspaceMigrationActionRunnerArgs, ): Promise { const { action, queryRunner } = context; - const { serverlessFunctionId } = action; + const { serverlessFunctionId, code } = action; const serverlessFunctionRepository = queryRunner.manager.getRepository( @@ -65,11 +42,57 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra serverlessFunctionId, fromWorkspaceMigrationUpdateActionToPartialEntity(action), ); + + const serverlessFunction = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: serverlessFunctionId, + flatEntityMaps: context.allFlatEntityMaps.flatServerlessFunctionMaps, + }); + + for (const update of action.updates) { + if (update.property === 'checksum' && isDefined(code)) { + await this.handleChecksumUpdate({ + serverlessFunction, + code, + }); + } + if (update.property === 'deletedAt' && isDefined(update.to)) { + await this.handleDeletedAtUpdate({ + serverlessFunction, + }); + } + } } - async executeForWorkspaceSchema( - _context: WorkspaceMigrationActionRunnerArgs, - ): Promise { - return; + async handleDeletedAtUpdate({ + serverlessFunction, + }: { + serverlessFunction: FlatServerlessFunction; + }) { + this.serverlessService.delete( + serverlessFunction as ServerlessFunctionEntity, + ); + } + + async handleChecksumUpdate({ + serverlessFunction, + code, + }: { + serverlessFunction: FlatServerlessFunction; + code: JSON; + }) { + const fileFolder = getServerlessFolder({ + serverlessFunction, + version: 'draft', + }); + + for (const key of Object.keys(code)) { + await this.fileStorageService.write({ + // @ts-expect-error legacy noImplicitAny + file: code[key], + name: basename(key), + mimeType: undefined, + folder: join(fileFolder, dirname(key)), + }); + } } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts index 18d7798c7b..bdf2cb7196 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts @@ -15,6 +15,10 @@ export interface WorkspaceMigrationRunnerActionHandlerService< execute( context: WorkspaceMigrationActionRunnerArgs>, ): Promise>; + + rollback( + context: WorkspaceMigrationActionRunnerArgs>, + ): Promise; } export type OptimisticallyApplyActionOnAllFlatEntityMapsArgs< @@ -28,19 +32,34 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService< TActionType extends WorkspaceMigrationActionTypeV2, > implements WorkspaceMigrationRunnerActionHandlerService { - abstract executeForMetadata( + executeForMetadata( + // eslint-disable-next-line unused-imports/no-unused-vars context: WorkspaceMigrationActionRunnerArgs>, - ): Promise; + ): Promise { + return Promise.resolve(); + } - abstract executeForWorkspaceSchema( + executeForWorkspaceSchema( + // eslint-disable-next-line unused-imports/no-unused-vars context: WorkspaceMigrationActionRunnerArgs>, - ): Promise; + ): Promise { + return Promise.resolve(); + } - abstract optimisticallyApplyActionOnAllFlatEntityMaps( + optimisticallyApplyActionOnAllFlatEntityMaps( args: OptimisticallyApplyActionOnAllFlatEntityMapsArgs< ExtractAction >, - ): Partial; + ): Partial { + return args.allFlatEntityMaps; + } + + rollbackForMetadata( + // eslint-disable-next-line unused-imports/no-unused-vars + context: WorkspaceMigrationActionRunnerArgs>, + ): Promise { + return Promise.resolve(); + } async execute( context: WorkspaceMigrationActionRunnerArgs>, @@ -55,6 +74,12 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService< allFlatEntityMaps: context.allFlatEntityMaps, }); } + + async rollback( + context: WorkspaceMigrationActionRunnerArgs>, + ): Promise { + await this.rollbackForMetadata(context); + } } export function WorkspaceMigrationRunnerActionHandler< diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service.ts index c1beb78a63..9c71f3ddf2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service.ts @@ -1,6 +1,8 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { DiscoveryService } from '@nestjs/core'; +import { isDefined } from 'twenty-shared/utils'; + import { type WorkspaceMigrationRunnerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; @@ -52,10 +54,15 @@ export class WorkspaceMigrationRunnerActionHandlerRegistryService }); } - async executeActionHandler( - actionType: WorkspaceMigrationActionTypeV2, - context: WorkspaceMigrationActionRunnerArgs, - ): Promise> { + async executeActionHandler({ + actionType, + context, + rollback, + }: { + actionType: WorkspaceMigrationActionTypeV2; + context: WorkspaceMigrationActionRunnerArgs; + rollback?: boolean; + }): Promise> { const handler = this.actionHandlers.get(actionType); if (!handler) { @@ -65,6 +72,12 @@ export class WorkspaceMigrationRunnerActionHandlerRegistryService ); } + if (isDefined(rollback) && rollback) { + await handler.rollback(context); + + return {}; + } + return await handler.execute(context); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts index b5bf2b08c0..905a62532a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts @@ -45,12 +45,14 @@ export class WorkspaceMigrationRunnerV2Service { for (const action of actions) { const partialOptimisticCache = await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler( - action.type, { - action, - allFlatEntityMaps, - queryRunner, - workspaceId, + actionType: action.type, + context: { + action, + allFlatEntityMaps, + queryRunner, + workspaceId, + }, }, ); const optimisticallyUpdatedFlatEntityMapsKeys = Object.keys( @@ -87,6 +89,24 @@ export class WorkspaceMigrationRunnerV2Service { console.trace(`Failed to rollback transaction: ${error.message}`); } } + + const invertedActions = actions.reverse(); + + for (const invertedAction of invertedActions) { + await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler( + { + actionType: invertedAction.type, + context: { + action: invertedAction, + allFlatEntityMaps: allFlatEntityMaps, + queryRunner, + workspaceId, + }, + rollback: true, + }, + ); + } + throw new WorkspaceQueryRunnerException( error.message, WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,