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,