Refactor workflow-logic-function-interaction (#17699)

# Refactor workflow–logic function interaction

## Why

Workflow code steps and standalone logic functions shared the same build
layer and DB layer, which blurred two use cases: code steps belong to a
workflow version; standalone functions are deployable units. That made
workflow code steps harder to own and evolve.

## Goal

Treat code steps as **workflow-owned**: build and run them in workflow
context, and expose workflow-scoped APIs so the editor can load, test,
and save code step source without going through the generic
logic-function layer.
This commit is contained in:
Charles Bochet
2026-02-05 00:46:55 +00:00
committed by GitHub
parent 752359335e
commit 67a98f77e3
77 changed files with 1750 additions and 1336 deletions
@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
@Module({
providers: [LogicFunctionBuildService],
exports: [LogicFunctionBuildService],
})
export class LogicFunctionBuildModule {}
@@ -1,160 +0,0 @@
import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import fs from 'fs/promises';
import { dirname, join } from 'path';
import { build } from 'esbuild';
import { FileFolder } from 'twenty-shared/types';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
export type FunctionBuildParams = {
flatLogicFunction: FlatLogicFunction;
applicationUniversalIdentifier: string;
};
@Injectable()
export class LogicFunctionBuildService {
constructor(private readonly fileStorageService: FileStorageService) {}
async hasLayerDependencies({
flatApplication,
applicationUniversalIdentifier,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
}): Promise<boolean> {
const packageJsonExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
});
const yarnLockExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
});
return packageJsonExists && yarnLockExists;
}
async uploadDependencies({
flatApplication: _flatApplication,
applicationUniversalIdentifier: _applicationUniversalIdentifier,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
}) {
// Package files live in Dependencies; no copy needed drivers read from
// Dependencies when building the layer.
}
async isBuilt({
flatLogicFunction,
applicationUniversalIdentifier,
}: FunctionBuildParams): Promise<boolean> {
return await this.fileStorageService.checkFileExists_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
});
}
async buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
}: FunctionBuildParams): Promise<{ checksum: string }> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.downloadFolder_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
const relativeSourcePath = getRelativePathFromBase(
flatLogicFunction.sourceHandlerPath,
baseFolderPath,
);
const relativeBuiltPath = getRelativePathFromBase(
flatLogicFunction.builtHandlerPath,
baseFolderPath,
);
const builtBundleFilePath = await this.buildInMemory({
sourceTemporaryDir,
sourceHandlerPath: relativeSourcePath,
builtHandlerPath: relativeBuiltPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
await this.fileStorageService.writeFile_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
sourceFile: builtFile,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
return {
checksum: crypto.createHash('md5').update(builtFile).digest('hex'),
};
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
private async buildInMemory({
sourceTemporaryDir,
sourceHandlerPath,
builtHandlerPath,
}: {
sourceTemporaryDir: string;
sourceHandlerPath: string;
builtHandlerPath: string;
}): Promise<string> {
const entryFilePath = join(sourceTemporaryDir, sourceHandlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath);
await fs.mkdir(dirname(builtBundleFilePath), { recursive: true });
await build({
entryPoints: [entryFilePath],
outfile: builtBundleFilePath,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
});
return builtBundleFilePath;
}
}
@@ -0,0 +1 @@
export const NODE_LAYER_SUBFOLDER = 'nodejs';
@@ -1,4 +0,0 @@
export const SEED_PROJECT_INPUT_SCHEMA = {
a: null,
b: null,
};
@@ -30,13 +30,11 @@ import {
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { NODE_LAYER_SUBFOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/lambda-layer.constant';
import { copyExecutor } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-executor';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-yarn-engine-and-build-dependencies';
import { createZipFile } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/create-zip-file';
import {
LambdaBuildDirectoryManager,
NODE_LAYER_SUBFOLDER,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
@@ -193,9 +191,9 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
return listLayerResult.LayerVersions[0].LayerVersionArn;
}
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
const buildTemporaryDirectoryManager = new LambdaBuildDirectoryManager();
const { sourceTemporaryDir, lambdaZipPath } =
await lambdaBuildDirectoryManager.init();
await buildTemporaryDirectoryManager.init();
const nodeDependenciesFolder = join(
sourceTemporaryDir,
@@ -226,7 +224,7 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
const result = await (await this.getLambdaClient()).send(command);
await lambdaBuildDirectoryManager.clean();
await buildTemporaryDirectoryManager.clean();
if (!isDefined(result.LayerVersionArn)) {
throw new Error('new layer version arn if undefined');
@@ -308,10 +306,10 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
applicationUniversalIdentifier,
});
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
const buildTemporaryDirectoryManager = new LambdaBuildDirectoryManager();
const { sourceTemporaryDir, lambdaZipPath } =
await lambdaBuildDirectoryManager.init();
await buildTemporaryDirectoryManager.init();
await copyExecutor(sourceTemporaryDir);
@@ -333,7 +331,7 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
await (await this.getLambdaClient()).send(command);
await lambdaBuildDirectoryManager.clean();
await buildTemporaryDirectoryManager.clean();
}
private extractLogs(logString: string): string {
@@ -1,6 +1,7 @@
import { promises as fs } from 'fs';
import { spawn } from 'node:child_process';
import { join } from 'path';
import { dirname } from 'node:path';
import { FileFolder } from 'twenty-shared/types';
@@ -12,15 +13,12 @@ import {
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-yarn-engine-and-build-dependencies';
import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { getRelativePathFromBase } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/utils/get-code-step-handler-path.util';
export interface LocalDriverOptions {
fileStorageService: FileStorageService;
@@ -122,9 +120,7 @@ export class LocalDriver implements LogicFunctionExecutorDriver {
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.builtHandlerPath,
);
const baseFolderPath = dirname(flatLogicFunction.builtHandlerPath);
await this.fileStorageService.downloadFolder_v2({
workspaceId: flatLogicFunction.workspaceId,
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { LogicFunctionBuildModule } from 'src/engine/core-modules/logic-function/logic-function-build/logic-function-build.module';
import { AddPackagesCommand } from 'src/engine/core-modules/logic-function/logic-function-executor/commands/add-packages.command';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
@@ -21,7 +20,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
SecretEncryptionModule,
SubscriptionsModule,
WorkspaceCacheModule,
LogicFunctionBuildModule,
FileModule,
TypeOrmModule.forFeature([LogicFunctionEntity]),
],
@@ -14,14 +14,12 @@ import {
type LogicFunctionExecuteParams,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-executor-driver.interface';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/build-env-var';
import { LOGIC_FUNCTION_EXECUTOR_DRIVER } from 'src/engine/core-modules/logic-function/logic-function-executor/constants/logic-function-executor.constants';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
@@ -30,7 +28,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -65,7 +62,6 @@ export class LogicFunctionExecutorService
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationTokenService: ApplicationTokenService,
private readonly secretEncryptionService: SecretEncryptionService,
private readonly functionBuildService: LogicFunctionBuildService,
private readonly subscriptionService: SubscriptionService,
private readonly auditService: AuditService,
private readonly fileStorageService: FileStorageService,
@@ -178,30 +174,17 @@ export class LogicFunctionExecutorService
}
if (
!(await this.functionBuildService.hasLayerDependencies({
!(await this.hasLayerDependencies({
flatApplication,
applicationUniversalIdentifier,
}))
) {
await this.functionBuildService.uploadDependencies({
flatApplication,
applicationUniversalIdentifier,
});
throw new LogicFunctionExecutionException(
'Logic function dependencies not found',
LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
if (
!(await this.functionBuildService.isBuilt({
flatLogicFunction,
applicationUniversalIdentifier,
}))
) {
await this.functionBuildService.buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
});
}
// END TODO
const resultLogicFunction = await this.callWithTimeout({
callback: () =>
this.execute({
@@ -251,55 +234,6 @@ export class LogicFunctionExecutorService
return resultLogicFunction;
}
async getLogicFunctionSourceCode(workspaceId: string, id: string) {
try {
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
'flatApplicationMaps',
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new LogicFunctionExecutionException(
`Application universal identifier not found for logic function ${id}`,
LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
return await this.fileStorageService.readFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
} catch (error) {
if (
isDefined(error) &&
'code' in error &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
return;
}
throw error;
}
}
async getAvailablePackages(logicFunctionId: string) {
const logicFunction = await this.logicFunctionRepository.findOneOrFail({
where: { id: logicFunctionId },
@@ -339,4 +273,27 @@ export class LogicFunctionExecutorService
),
]);
}
private async hasLayerDependencies({
flatApplication,
applicationUniversalIdentifier,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
}): Promise<boolean> {
const packageJsonExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
});
const yarnLockExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatApplication.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
});
return packageJsonExists && yarnLockExists;
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LogicFunctionSourceBuilderService } from './logic-function-source-builder.service';
@Module({
providers: [LogicFunctionSourceBuilderService],
exports: [LogicFunctionSourceBuilderService],
})
export class LogicFunctionSourceBuilderModule {}
@@ -0,0 +1,375 @@
import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import fs from 'fs/promises';
import { dirname, join } from 'path';
import { isObject } from '@sniptt/guards';
import { build } from 'esbuild';
import { FileFolder, Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-source-builder/utils/get-logic-function-handler-path.util';
import {
getLogicFunctionSeedProjectFiles,
LogicFunctionSeedProjectFile,
} from 'src/engine/core-modules/logic-function/logic-function-source-builder/utils/get-logic-function-seed-project-files.util';
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_SOURCE_HANDLER_PATH,
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
type SeedSourceFilesParams = {
logicFunctionId: string;
workspaceId: string;
applicationUniversalIdentifier: string;
code?: Sources;
};
type SeedSourceFilesResult = {
sourceHandlerPath: string;
builtHandlerPath: string;
checksum: string;
};
type UpdateSourceFilesParams = {
sourceHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
code: Sources;
};
type BuildFromSourceParams = {
sourceHandlerPath: string;
builtHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
type GetSourceCodeParams = {
sourceHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
type CopySourceAndBuiltParams = {
fromSourceHandlerPath: string;
fromBuiltHandlerPath: string;
toSourceHandlerPath: string;
toBuiltHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
@Injectable()
export class LogicFunctionSourceBuilderService {
constructor(private readonly fileStorageService: FileStorageService) {}
async seedSourceFiles({
logicFunctionId,
workspaceId,
applicationUniversalIdentifier,
code,
}: SeedSourceFilesParams): Promise<SeedSourceFilesResult> {
const sourceHandlerPath = `${logicFunctionId}/${DEFAULT_SOURCE_HANDLER_PATH}`;
const builtHandlerPath = `${logicFunctionId}/${DEFAULT_BUILT_HANDLER_PATH}`;
if (isDefined(code)) {
// Use provided code
await this.updateSourceFiles({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
code,
});
const { checksum } = await this.buildFromSource({
sourceHandlerPath,
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
});
return {
sourceHandlerPath,
builtHandlerPath,
checksum,
};
}
// Use seed project files
const seedProjectFiles = await getLogicFunctionSeedProjectFiles();
const sourceFiles = seedProjectFiles.filter(
(file: LogicFunctionSeedProjectFile) => file.name.endsWith('index.ts'),
);
const builtFiles = seedProjectFiles.filter(
(file: LogicFunctionSeedProjectFile) => file.name.endsWith('.mjs'),
);
if (sourceFiles.length !== 1 || builtFiles.length !== 1) {
throw new LogicFunctionException(
'Logic function seed project should have one index.ts file and one index.mjs file',
LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT,
);
}
const sourceFile = sourceFiles[0];
const builtFile = builtFiles[0];
await this.fileStorageService.writeFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: sourceHandlerPath,
sourceFile: sourceFile.content,
mimeType: 'application/typescript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.fileStorageService.writeFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: builtHandlerPath,
sourceFile: builtFile.content,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
const checksum = crypto
.createHash('md5')
.update(builtFile.content)
.digest('hex');
return {
sourceHandlerPath,
builtHandlerPath,
checksum,
};
}
async updateSourceFiles({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
code,
}: UpdateSourceFilesParams): Promise<void> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
await this.writeSourcesToLocalFolder(code, sourceTemporaryDir);
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
await this.fileStorageService.uploadFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
async buildFromSource({
sourceHandlerPath,
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: BuildFromSourceParams): Promise<{ checksum: string }> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
await this.fileStorageService.downloadFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
const relativeSourcePath = getRelativePathFromBase(
sourceHandlerPath,
baseFolderPath,
);
const relativeBuiltPath = getRelativePathFromBase(
builtHandlerPath,
baseFolderPath,
);
const builtBundleFilePath = await this.buildInMemory({
sourceTemporaryDir,
sourceHandlerPath: relativeSourcePath,
builtHandlerPath: relativeBuiltPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
await this.fileStorageService.writeFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: builtHandlerPath,
sourceFile: builtFile,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
return {
checksum: crypto.createHash('md5').update(builtFile).digest('hex'),
};
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
async getSourceCode({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: GetSourceCodeParams): Promise<Sources | null> {
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
try {
return await this.fileStorageService.readFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
} catch (error) {
if (
isDefined(error) &&
typeof error === 'object' &&
'code' in error &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
return null;
}
throw error;
}
}
async copySourceAndBuilt({
fromSourceHandlerPath,
fromBuiltHandlerPath,
toSourceHandlerPath,
toBuiltHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: CopySourceAndBuiltParams): Promise<void> {
const fromSourceBaseFolderPath = getLogicFunctionBaseFolderPath(
fromSourceHandlerPath,
);
const toSourceBaseFolderPath =
getLogicFunctionBaseFolderPath(toSourceHandlerPath);
const fromBuiltBaseFolderPath =
getLogicFunctionBaseFolderPath(fromBuiltHandlerPath);
const toBuiltBaseFolderPath =
getLogicFunctionBaseFolderPath(toBuiltHandlerPath);
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromSourceBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toSourceBaseFolderPath,
},
});
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: fromBuiltBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: toBuiltBaseFolderPath,
},
});
}
private async writeSourcesToLocalFolder(
sources: Sources,
localPath: string,
): Promise<void> {
for (const key of Object.keys(sources)) {
const filePath = join(localPath, key);
const value = sources[key];
if (isObject(value)) {
await this.writeSourcesToLocalFolder(value as Sources, filePath);
continue;
}
await fs.mkdir(dirname(filePath), { recursive: true });
await fs.writeFile(filePath, value);
}
}
private async buildInMemory({
sourceTemporaryDir,
sourceHandlerPath,
builtHandlerPath,
}: {
sourceTemporaryDir: string;
sourceHandlerPath: string;
builtHandlerPath: string;
}): Promise<string> {
const entryFilePath = join(sourceTemporaryDir, sourceHandlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath);
await fs.mkdir(dirname(builtBundleFilePath), { recursive: true });
await build({
entryPoints: [entryFilePath],
outfile: builtBundleFilePath,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
});
return builtBundleFilePath;
}
}
@@ -1,15 +1,19 @@
import fs from 'fs/promises';
import path, { join } from 'path';
import path from 'path';
import { ASSET_PATH } from 'src/constants/assets-path';
type File = { name: string; path: string; content: Buffer };
export type LogicFunctionSeedProjectFile = {
name: string;
path: string;
content: Buffer;
};
const getAllFiles = async (
rootDir: string,
dir: string = rootDir,
files: File[] = [],
): Promise<File[]> => {
files: LogicFunctionSeedProjectFile[] = [],
): Promise<LogicFunctionSeedProjectFile[]> => {
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of dirEntries) {
@@ -29,11 +33,13 @@ const getAllFiles = async (
return files;
};
export const getSeedProjectFiles = (async () => {
const seedProjectPath = join(
export const getLogicFunctionSeedProjectFiles = async (): Promise<
LogicFunctionSeedProjectFile[]
> => {
const seedProjectPath = path.join(
ASSET_PATH,
`engine/core-modules/logic-function/logic-function-drivers/constants/seed-project`,
'engine/core-modules/logic-function/logic-function-source-builder/constants/seed-project',
);
return await getAllFiles(seedProjectPath);
})();
return getAllFiles(seedProjectPath);
};
@@ -2,9 +2,9 @@ import { type DynamicModule, Global, Module } from '@nestjs/common';
import { type LogicFunctionExecutorModuleAsyncOptions } from 'src/engine/core-modules/logic-function/logic-function-executor/interfaces/logic-function-executor.interface';
import { LogicFunctionBuildModule } from 'src/engine/core-modules/logic-function/logic-function-build/logic-function-build.module';
import { LogicFunctionDriversModule } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-drivers.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { LogicFunctionSourceBuilderModule } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.module';
import { LogicFunctionTriggerModule } from 'src/engine/core-modules/logic-function/logic-function-trigger/logic-function-trigger.module';
@Global()
@@ -18,13 +18,13 @@ export class LogicFunctionModule {
imports: [
LogicFunctionDriversModule.forRootAsync(options),
LogicFunctionExecutorModule,
LogicFunctionBuildModule,
LogicFunctionSourceBuilderModule,
LogicFunctionTriggerModule,
],
exports: [
LogicFunctionDriversModule,
LogicFunctionExecutorModule,
LogicFunctionBuildModule,
LogicFunctionSourceBuilderModule,
LogicFunctionTriggerModule,
],
};