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:
-9
@@ -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 {}
|
||||
-160
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const NODE_LAYER_SUBFOLDER = 'nodejs';
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export const SEED_PROJECT_INPUT_SCHEMA = {
|
||||
a: null,
|
||||
b: null,
|
||||
};
|
||||
+8
-10
@@ -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 {
|
||||
|
||||
+3
-7
@@ -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,
|
||||
|
||||
-2
@@ -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]),
|
||||
],
|
||||
|
||||
+29
-72
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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 {}
|
||||
+375
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -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);
|
||||
};
|
||||
+3
-3
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user