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:
-9
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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')
|
||||
|
||||
-24
@@ -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 },
|
||||
|
||||
+133
-57
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+13
-6
@@ -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(),
|
||||
|
||||
+12
@@ -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}/`, '');
|
||||
};
|
||||
Reference in New Issue
Block a user