Refactor workflow to use new functions (#17552)

Removes the versioning system for logic functions (`latestVersion`,
`publishedVersions`) and simplifies the file storage structure.

### Changes
- Remove `publishOneLogicFunctionOrFail` and publishing logic from
workflow status updates
- Add `createLogicFunctionFromExistingLogicFunction` to duplicate logic
functions when creating draft workflow versions
- Update `createDraftStep` to create a new logic function copy instead
of referencing the same one
- Migrate file storage to v2 endpoints with
`applicationUniversalIdentifier`
- Unify path structure: source files at `source/workflow/{id}/`, built
files at `built-logic-function/workflow/{id}/`
- Store full paths in `sourceHandlerPath` and `builtHandlerPath` entity
fields
This commit is contained in:
Charles Bochet
2026-01-30 15:42:36 +01:00
committed by GitHub
parent d624652e36
commit 5996d0fc03
43 changed files with 1213 additions and 1604 deletions
@@ -1,9 +0,0 @@
import { ID, InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
@InputType()
export class PublishLogicFunctionInput {
@IDField(() => ID, { description: 'The id of the function.' })
id!: string;
}
@@ -41,7 +41,7 @@ export enum LogicFunctionRuntime {
}
export const DEFAULT_SOURCE_HANDLER_PATH = 'src/index.ts';
export const DEFAULT_BUILT_HANDLER_PATH = 'index.mjs';
export const DEFAULT_BUILT_HANDLER_PATH = 'src/index.mjs';
export const DEFAULT_HANDLER_NAME = 'main';
@Entity('logicFunction')
@@ -16,7 +16,6 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
import { CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import { ExecuteLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/execute-logic-function.input';
import { GetLogicFunctionSourceCodeInput } from 'src/engine/metadata-modules/logic-function/dtos/get-logic-function-source-code.input';
import { PublishLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/publish-logic-function.input';
import { LogicFunctionExecutionResultDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionIdInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-id.input';
import { LogicFunctionLogsDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.dto';
@@ -210,29 +209,6 @@ export class LogicFunctionResolver {
}
}
@Mutation(() => LogicFunctionDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async publishLogicFunction(
@Args('input') input: PublishLogicFunctionInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
const { id } = input;
const flatLogicFunction =
await this.logicFunctionService.publishOneLogicFunctionOrFail(
id,
workspaceId,
);
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction,
});
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
}
@Subscription(() => LogicFunctionLogsDTO, {
filter: (
payload: { logicFunctionLogs: LogicFunctionLogsDTO },
@@ -5,6 +5,7 @@ import {
DEFAULT_API_KEY_NAME,
DEFAULT_API_URL_NAME,
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -19,9 +20,9 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { buildEnvVar } from 'src/engine/core-modules/logic-function-executor/drivers/utils/build-env-var';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.service';
import { getLogicFunctionFolderOrThrow } from 'src/engine/core-modules/logic-function-executor/utils/get-logic-function-folder-or-throw.utils';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-base-folder-path.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { LogicFunctionLayerService } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.service';
@@ -68,25 +69,42 @@ export class LogicFunctionService {
) {}
async getLogicFunctionSourceCode(workspaceId: string, id: string) {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
try {
const folderPath = getLogicFunctionFolderOrThrow({
flatLogicFunction,
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
'flatApplicationMaps',
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return await this.fileStorageService.readFolder(folderPath);
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${id}`,
LogicFunctionExceptionCode.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 (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
return;
@@ -166,14 +184,30 @@ export class LogicFunctionService {
...buildEnvVar(flatApplicationVariables, this.secretEncryptionService),
};
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${flatLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
// We keep that check to build functions
if (
!(await this.functionBuildService.isBuilt({
flatLogicFunction,
applicationUniversalIdentifier,
}))
) {
await this.functionBuildService.buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
});
}
@@ -182,6 +216,7 @@ export class LogicFunctionService {
this.logicFunctionExecutorService.execute({
flatLogicFunction,
flatLogicFunctionLayer,
applicationUniversalIdentifier,
payload,
env: envVariables,
}),
@@ -193,13 +228,6 @@ export class LogicFunctionService {
console.log(resultLogicFunction.logs);
}
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
await this.subscriptionService.publish({
channel: SubscriptionChannel.LOGIC_FUNCTION_LOGS_CHANNEL,
workspaceId,
@@ -232,26 +260,6 @@ export class LogicFunctionService {
return resultLogicFunction;
}
async publishOneLogicFunctionOrFail(
id: string,
workspaceId: string,
): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingFlatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return existingFlatLogicFunction;
}
async deleteOneLogicFunction({
id,
workspaceId,
@@ -556,6 +564,19 @@ export class LogicFunctionService {
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
return this.createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
@@ -565,32 +586,87 @@ export class LogicFunctionService {
},
);
const flatLogicFunctionToDuplicate = findFlatLogicFunctionOrThrow({
const existingLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return this.createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
}: {
existingLogicFunction: FlatLogicFunction;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatApplicationMaps',
]);
const existingApplicationUniversalIdentifier = isDefined(
existingLogicFunction.applicationId,
)
? flatApplicationMaps.byId[existingLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(existingApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${existingLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const newFlatLogicFunction = await this.createOneLogicFunction(
{
name: flatLogicFunctionToDuplicate.name,
description: flatLogicFunctionToDuplicate.description ?? undefined,
timeoutSeconds: flatLogicFunctionToDuplicate.timeoutSeconds,
applicationId: flatLogicFunctionToDuplicate.applicationId ?? undefined,
logicFunctionLayerId: flatLogicFunctionToDuplicate.logicFunctionLayerId,
name: existingLogicFunction.name,
description: existingLogicFunction.description ?? undefined,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
applicationId: existingLogicFunction.applicationId ?? undefined,
logicFunctionLayerId: existingLogicFunction.logicFunctionLayerId,
},
workspaceId,
);
await this.fileStorageService.copy({
const newApplicationUniversalIdentifier = isDefined(
newFlatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[newFlatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(newApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${newFlatLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const fromBaseFolderPath = getLogicFunctionBaseFolderPath(
existingLogicFunction.sourceHandlerPath,
);
const toBaseFolderPath = getLogicFunctionBaseFolderPath(
newFlatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.copy_v2({
from: {
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction: flatLogicFunctionToDuplicate,
}),
workspaceId,
applicationUniversalIdentifier: existingApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromBaseFolderPath,
},
to: {
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction: newFlatLogicFunction,
}),
workspaceId,
applicationUniversalIdentifier: newApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toBaseFolderPath,
},
});
@@ -12,6 +12,8 @@ import {
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils';
const WORKFLOW_BASE_FOLDER_PREFIX = 'workflow';
export type FromCreateLogicFunctionInputToFlatLogicFunctionArgs = {
createLogicFunctionInput: CreateLogicFunctionInput & {
logicFunctionLayerId: string;
@@ -28,6 +30,15 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
const id = v4();
const currentDate = new Date();
// Build full paths including the base folder
const baseFolder = `${WORKFLOW_BASE_FOLDER_PREFIX}/${id}`;
const sourceHandlerPath =
rawCreateLogicFunctionInput.sourceHandlerPath ??
`${baseFolder}/${DEFAULT_SOURCE_HANDLER_PATH}`;
const builtHandlerPath =
rawCreateLogicFunctionInput.builtHandlerPath ??
`${baseFolder}/${DEFAULT_BUILT_HANDLER_PATH}`;
return {
id,
cronTriggerSettings: null,
@@ -35,14 +46,10 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
httpRouteTriggerSettings: null,
name: rawCreateLogicFunctionInput.name,
description: rawCreateLogicFunctionInput.description ?? null,
sourceHandlerPath:
rawCreateLogicFunctionInput.sourceHandlerPath ??
DEFAULT_SOURCE_HANDLER_PATH,
sourceHandlerPath,
handlerName:
rawCreateLogicFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME,
builtHandlerPath:
rawCreateLogicFunctionInput.builtHandlerPath ??
DEFAULT_BUILT_HANDLER_PATH,
builtHandlerPath,
universalIdentifier:
rawCreateLogicFunctionInput.universalIdentifier ?? v4(),
createdAt: currentDate.toISOString(),
@@ -0,0 +1,12 @@
import { dirname } from 'path';
export const getLogicFunctionBaseFolderPath = (handlerPath: string): string => {
return dirname(dirname(handlerPath));
};
export const getRelativePathFromBase = (
handlerPath: string,
baseFolderPath: string,
): string => {
return handlerPath.replace(`${baseFolderPath}/`, '');
};