Logic function refactorization (#17861)

As title
This commit is contained in:
martmull
2026-02-12 11:40:49 +01:00
committed by GitHub
parent b456f79167
commit a4ed043d43
122 changed files with 1441 additions and 1897 deletions
@@ -38,8 +38,9 @@ import {
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application-layer/utils/copy-yarn-engine-and-build-dependencies';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application/utils/copy-yarn-engine-and-build-dependencies';
import { type LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
import { callWithTimeout } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/call-with-timeout';
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h
@@ -322,6 +323,7 @@ export class LambdaDriver implements LogicFunctionDriver {
applicationUniversalIdentifier,
payload,
env,
timeoutMs = 900_000,
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
await this.build({
flatLogicFunction,
@@ -355,7 +357,12 @@ export class LambdaDriver implements LogicFunctionDriver {
const command = new InvokeCommand(params);
try {
const result = await (await this.getLambdaClient()).send(command);
const lambdaClient = await this.getLambdaClient();
const result = await callWithTimeout({
callback: () => lambdaClient.send(command),
timeoutMs,
});
const parsedResult = result.Payload
? JSON.parse(result.Payload.transformToString())
@@ -13,7 +13,7 @@ import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/l
import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console';
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application-layer/utils/copy-yarn-engine-and-build-dependencies';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application/utils/copy-yarn-engine-and-build-dependencies';
import type { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
export interface LocalDriverOptions {
@@ -76,6 +76,7 @@ export class LocalDriver implements LogicFunctionDriver {
applicationUniversalIdentifier,
payload,
env,
timeoutMs = 900_000,
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
await this.build({
flatApplication,
@@ -89,17 +90,13 @@ export class LocalDriver implements LogicFunctionDriver {
try {
const { sourceTemporaryDir } = await temporaryDirManager.init();
const inMemoryBuiltHandlerPath = join(
sourceTemporaryDir,
flatLogicFunction.builtHandlerPath,
);
await this.logicFunctionResourceService.copyBuiltCodeInMemory({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
inMemoryDestinationPath: inMemoryBuiltHandlerPath,
});
const inMemoryBuiltHandlerPath =
await this.logicFunctionResourceService.copyBuiltCodeInMemory({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
inMemoryDestinationPath: sourceTemporaryDir,
});
try {
await fs.symlink(
@@ -161,7 +158,7 @@ export class LocalDriver implements LogicFunctionDriver {
runnerPath,
env: env ?? {},
payload,
timeoutMs: 900_000, // timeout is handled by the logic function service
timeoutMs,
});
if (stdout)
@@ -26,6 +26,7 @@ export type LogicFunctionExecuteParams = {
applicationUniversalIdentifier: string;
payload: object;
env?: Record<string, string>;
timeoutMs?: number;
};
export interface LogicFunctionDriver {
@@ -0,0 +1,14 @@
export const callWithTimeout = async <T>({
callback,
timeoutMs,
}: {
callback: () => Promise<T>;
timeoutMs: number;
}): Promise<T> => {
return Promise.race([
callback(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs),
),
]);
};
@@ -83,16 +83,13 @@ export class LogicFunctionExecutorService {
flatLogicFunction,
});
const resultLogicFunction = await this.callWithTimeout({
callback: () =>
this.driver.execute({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
payload,
env: envVariables,
}),
timeoutMs: flatLogicFunction.timeoutSeconds * 1000,
const resultLogicFunction = await this.driver.execute({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
payload,
env: envVariables,
timeoutMs: flatLogicFunction.timeoutSeconds * 1_000,
});
await this.handleExecutionResult({
@@ -121,21 +118,6 @@ export class LogicFunctionExecutorService {
}
}
private async callWithTimeout<T>({
callback,
timeoutMs,
}: {
callback: () => Promise<T>;
timeoutMs: number;
}): Promise<T> {
return Promise.race([
callback(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs),
),
]);
}
private async getFlatEntitiesOrThrow({
workspaceId,
logicFunctionId,
@@ -4,9 +4,8 @@ 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 { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { NODE_ESM_CJS_BANNER } from 'twenty-shared/application';
@@ -14,64 +13,56 @@ import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/i
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-resource/utils/get-logic-function-handler-path.util';
import {
getLogicFunctionSeedProjectFiles,
LogicFunctionSeedProjectFile,
} from 'src/engine/core-modules/logic-function/logic-function-resource/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';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
type SeedSourceFilesParams = {
type Identifier = {
workspaceId: string;
applicationUniversalIdentifier: string;
sourceSubfolder: string;
};
type SeedSourceFilesParams = Identifier & {
sourceHandlerPath: string;
builtHandlerPath: string;
};
type SeedSourceFilesResult = {
sourceHandlerPath: string;
builtHandlerPath: string;
handlerName: string;
checksum: string;
};
type UpdateSourceFilesParams = {
sourceHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
code: Sources;
type UpdateSourceFilesParams = Omit<
SeedSourceFilesParams,
'builtHandlerPath'
> & {
sourceHandlerCode: string;
};
type BuildFromSourceParams = {
type BuildFromSourceParams = Identifier & {
sourceHandlerPath: string;
builtHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
type GetSourceCodeParams = {
type GetSourceCodeParams = Identifier & {
sourceHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
type CopySourceParams = {
type GetBuiltCodeParams = Identifier & {
builtHandlerPath: string;
};
type CopySourceParams = Identifier & {
fromSourceHandlerPath: string;
fromBuiltHandlerPath: string;
toSourceHandlerPath: string;
fromBuiltHandlerPath: string;
toBuiltHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
};
@Injectable()
@@ -81,11 +72,9 @@ export class LogicFunctionResourceService {
async seedSourceFiles({
workspaceId,
applicationUniversalIdentifier,
sourceSubfolder,
sourceHandlerPath,
builtHandlerPath,
}: SeedSourceFilesParams): Promise<SeedSourceFilesResult> {
const sourceHandlerPath = `${sourceSubfolder}/${DEFAULT_SOURCE_HANDLER_PATH}`;
const builtHandlerPath = `${sourceSubfolder}/${DEFAULT_BUILT_HANDLER_PATH}`;
const seedProjectFiles = await getLogicFunctionSeedProjectFiles();
const sourceFiles = seedProjectFiles.filter(
@@ -138,40 +127,28 @@ export class LogicFunctionResourceService {
return {
handlerName: 'main',
sourceHandlerPath,
builtHandlerPath,
checksum,
};
}
async updateSourceFiles({
async uploadSourceFile({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
code,
sourceHandlerCode,
}: UpdateSourceFilesParams): Promise<void> {
const temporaryDirManager = new TemporaryDirManager();
try {
const { sourceTemporaryDir } = await temporaryDirManager.init();
await this.writeSourcesToLocalFolder(code, sourceTemporaryDir);
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
await this.fileStorageService.uploadFolder({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} finally {
await temporaryDirManager.clean();
}
await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: sourceHandlerPath,
sourceFile: sourceHandlerCode,
settings: { isTemporaryFile: false, toDelete: false },
mimeType: 'application/typescript',
});
}
async buildFromSource({
async buildFromSourceFile({
sourceHandlerPath,
builtHandlerPath,
workspaceId,
@@ -182,29 +159,18 @@ export class LogicFunctionResourceService {
try {
const { sourceTemporaryDir } = await temporaryDirManager.init();
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
await this.fileStorageService.downloadFolder({
await this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
resourcePath: sourceHandlerPath,
localPath: join(sourceTemporaryDir, sourceHandlerPath),
});
const relativeSourcePath = getRelativePathFromBase(
sourceHandlerPath,
baseFolderPath,
);
const relativeBuiltPath = getRelativePathFromBase(
builtHandlerPath,
baseFolderPath,
);
const builtBundleFilePath = await this.buildInMemory({
sourceTemporaryDir,
sourceHandlerPath: relativeSourcePath,
builtHandlerPath: relativeBuiltPath,
sourceHandlerPath,
builtHandlerPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
@@ -230,20 +196,22 @@ export class LogicFunctionResourceService {
}
}
async getSourceCode({
async getSourceFile({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: GetSourceCodeParams): Promise<Sources | null> {
const baseFolderPath = getLogicFunctionBaseFolderPath(sourceHandlerPath);
}: GetSourceCodeParams): Promise<string | null> {
try {
return await this.fileStorageService.readFolder({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
return (
await streamToBuffer(
await this.fileStorageService.readFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: sourceHandlerPath,
}),
)
).toString('utf-8');
} catch (error) {
if (
isDefined(error) &&
@@ -259,34 +227,24 @@ export class LogicFunctionResourceService {
async copyResources({
fromSourceHandlerPath,
fromBuiltHandlerPath,
toSourceHandlerPath,
fromBuiltHandlerPath,
toBuiltHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: CopySourceParams): Promise<void> {
const fromSourceBaseFolderPath = getLogicFunctionBaseFolderPath(
fromSourceHandlerPath,
);
const toSourceBaseFolderPath =
getLogicFunctionBaseFolderPath(toSourceHandlerPath);
const fromBuiltBaseFolderPath =
getLogicFunctionBaseFolderPath(fromBuiltHandlerPath);
const toBuiltBaseFolderPath =
getLogicFunctionBaseFolderPath(toBuiltHandlerPath);
await this.fileStorageService.copy({
from: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromSourceBaseFolderPath,
resourcePath: fromSourceHandlerPath,
},
to: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toSourceBaseFolderPath,
resourcePath: toSourceHandlerPath,
},
});
@@ -295,13 +253,13 @@ export class LogicFunctionResourceService {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: fromBuiltBaseFolderPath,
resourcePath: fromBuiltHandlerPath,
},
to: {
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: toBuiltBaseFolderPath,
resourcePath: toBuiltHandlerPath,
},
});
}
@@ -337,11 +295,7 @@ export class LogicFunctionResourceService {
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: {
builtHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
}): Promise<string> {
}: GetBuiltCodeParams): Promise<string> {
return (
await streamToBuffer(
await this.fileStorageService.readFile({
@@ -359,36 +313,20 @@ export class LogicFunctionResourceService {
workspaceId,
applicationUniversalIdentifier,
inMemoryDestinationPath,
}: {
builtHandlerPath: string;
workspaceId: string;
applicationUniversalIdentifier: string;
}: GetBuiltCodeParams & {
inMemoryDestinationPath: string;
}): Promise<void> {
}): Promise<string> {
const localPath = join(inMemoryDestinationPath, builtHandlerPath);
await this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: builtHandlerPath,
localPath: inMemoryDestinationPath,
localPath,
});
}
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);
}
return localPath;
}
private async buildInMemory({