diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts index caec666f9a5..daf50b722bb 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command.ts @@ -2,8 +2,13 @@ import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import crypto from 'crypto'; +import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { build } from 'esbuild'; import { Command } from 'nest-commander'; +import { NODE_ESM_CJS_BANNER } from 'twenty-shared/application'; import { FileFolder, type Sources } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; @@ -245,32 +250,86 @@ export class MigrateWorkflowCodeStepsCommand extends ActiveOrSuspendedWorkspaces ): Promise<{ builtContent: string; sourceContent: string } | null> { const workspacePrefix = `workspace-${workspaceId}`; - const builtSources = await this.fileStorageService.readFolderLegacy( - `${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`, - ); + let builtSources: Sources | null = null; - const sourceSources = await this.fileStorageService.readFolderLegacy( - `${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`, - ); - - // Old source layout may nest files under a `src/` key - const sourceRoot = - (sourceSources.src as Sources) ?? (sourceSources as Sources); - - const builtContent = builtSources['index.mjs'] as string; - const sourceContent = sourceRoot['index.ts'] as string; - - if (!isDefined(builtContent) || !isDefined(sourceContent)) { + try { + builtSources = await this.fileStorageService.readFolderLegacy( + `${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`, + ); + } catch { this.logger.warn( - `Missing index.mjs or index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`, + `Built folder not found for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, will build from source`, + ); + } + + let sourceSources: Sources; + + try { + sourceSources = await this.fileStorageService.readFolderLegacy( + `${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`, + ); + } catch { + this.logger.warn( + `Source folder not found for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`, ); return null; } + // Old source layout may nest files under a `src/` key + const sourceRoot = + (sourceSources.src as Sources) ?? (sourceSources as Sources); + + const sourceContent = sourceRoot['index.ts'] as string; + + if (!isDefined(sourceContent)) { + this.logger.warn( + `Missing index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}, skipping`, + ); + + return null; + } + + let builtContent = builtSources?.['index.mjs'] as string | undefined; + + if (!isDefined(builtContent)) { + this.logger.log( + `Building serverless function ${serverlessFunctionId}/${version} from source in workspace ${workspaceId}`, + ); + builtContent = await this.buildSourceToMjs(sourceContent); + } + return { builtContent, sourceContent }; } + private async buildSourceToMjs(sourceContent: string): Promise { + const tempDir = await mkdtemp(join(tmpdir(), 'twenty-migrate-')); + + try { + const srcDir = join(tempDir, 'src'); + const outFile = join(tempDir, 'index.mjs'); + + await mkdir(srcDir, { recursive: true }); + await writeFile(join(srcDir, 'index.ts'), sourceContent); + + await build({ + entryPoints: [join(srcDir, 'index.ts')], + outfile: outFile, + platform: 'node', + format: 'esm', + target: 'es2017', + bundle: true, + sourcemap: true, + packages: 'external', + banner: NODE_ESM_CJS_BANNER, + }); + + return await readFile(outFile, 'utf-8'); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + } + private async uploadFunctionFiles( workspaceId: string, applicationUniversalIdentifier: string,