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,15 +1,26 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { join } from 'path';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { Repository } from 'typeorm';
import { isObject } from '@sniptt/guards';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getSeedProjectFiles } from 'src/engine/core-modules/logic-function-executor/drivers/utils/get-seed-project-files';
import { getLogicFunctionFolderOrThrow } from 'src/engine/core-modules/logic-function-executor/utils/get-logic-function-folder-or-throw.utils';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function-executor/drivers/utils/lambda-build-directory-manager';
import { getLogicFunctionBaseFolderPath } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-base-folder-path.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { CreateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
@@ -23,6 +34,8 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
constructor(
private readonly fileStorageService: FileStorageService,
private readonly functionBuildService: FunctionBuildService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
@@ -33,7 +46,13 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
const { action, queryRunner, workspaceId } = context;
const { flatEntity: logicFunction } = action;
await this.buildAndSaveLogicFunction(logicFunction);
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(logicFunction.applicationId);
await this.buildAndSaveLogicFunction(
logicFunction,
applicationUniversalIdentifier,
);
const logicFunctionRepository =
queryRunner.manager.getRepository<LogicFunctionEntity>(
@@ -46,28 +65,84 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
});
}
private async buildAndSaveLogicFunction(logicFunction: FlatLogicFunction) {
const draftFileFolder = getLogicFunctionFolderOrThrow({
flatLogicFunction: logicFunction,
private async getApplicationUniversalIdentifier(
applicationId: string,
): Promise<string> {
const application = await this.applicationRepository.findOne({
where: { id: applicationId },
select: ['universalIdentifier'],
});
if (isDefined(logicFunction?.code)) {
await this.fileStorageService.writeFolder(
logicFunction.code,
draftFileFolder,
if (!isDefined(application)) {
throw new LogicFunctionException(
`Application with id ${applicationId} not found`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
} else {
for (const file of await getSeedProjectFiles) {
await this.fileStorageService.write({
file: file.content,
name: file.name,
mimeType: 'application/typescript',
folder: join(draftFileFolder, file.path),
});
}
}
return application.universalIdentifier;
}
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 buildAndSaveLogicFunction(
logicFunction: FlatLogicFunction,
applicationUniversalIdentifier: string,
) {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
if (isDefined(logicFunction?.code)) {
await this.writeSourcesToLocalFolder(
logicFunction.code,
sourceTemporaryDir,
);
} else {
for (const file of await getSeedProjectFiles) {
const filePath = join(sourceTemporaryDir, file.path, file.name);
await fs.mkdir(join(sourceTemporaryDir, file.path), {
recursive: true,
});
await fs.writeFile(filePath, file.content);
}
}
const baseFolderPath = getLogicFunctionBaseFolderPath(
logicFunction.sourceHandlerPath,
);
await this.fileStorageService.uploadFolder_v2({
workspaceId: logicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} finally {
await lambdaBuildDirectoryManager.clean();
}
await this.functionBuildService.buildAndUpload({
flatLogicFunction: logicFunction,
applicationUniversalIdentifier,
});
}
@@ -76,10 +151,20 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
): Promise<void> {
const { action } = context;
await this.fileStorageService.delete({
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction: action.flatEntity,
}),
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(
action.flatEntity.applicationId,
);
const baseFolderPath = getLogicFunctionBaseFolderPath(
action.flatEntity.sourceHandlerPath,
);
await this.fileStorageService.delete_v2({
workspaceId: action.flatEntity.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
}
}
@@ -5,9 +5,9 @@ import { FileFolder } from 'twenty-shared/types';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getLogicFunctionFolderOrThrow } from 'src/engine/core-modules/logic-function-executor/utils/get-logic-function-folder-or-throw.utils';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { getLogicFunctionBaseFolderPath } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-base-folder-path.util';
import { DeleteLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@@ -41,58 +41,19 @@ export class DeleteLogicFunctionActionHandlerService extends WorkspaceMigrationR
workspaceId,
});
// TODO: Should implement a cron task or a job to delete the files after a certain period of time
await this.fileStorageService.move({
from: {
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction,
fileFolder: FileFolder.LogicFunction,
}),
},
to: {
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction,
fileFolder: FileFolder.LogicFunctionToDelete,
}),
},
});
const sourceBaseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
const builtBaseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.builtHandlerPath,
);
// We can delete built code as it can be computed from source code if rollback occurs
await this.fileStorageService.delete({
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction,
fileFolder: FileFolder.BuiltFunction,
}),
});
const sourceFolderPath = `workspace-${workspaceId}/${FileFolder.Source}/${sourceBaseFolderPath}`;
const builtFolderPath = `workspace-${workspaceId}/${FileFolder.BuiltLogicFunction}/${builtBaseFolderPath}`;
await this.fileStorageService.delete({ folderPath: sourceFolderPath });
await this.fileStorageService.delete({ folderPath: builtFolderPath });
}
async rollbackForMetadata(
context: Omit<
WorkspaceMigrationActionRunnerArgs<DeleteLogicFunctionAction>,
'queryRunner'
>,
): Promise<void> {
const { action, allFlatEntityMaps } = context;
const { universalIdentifier } = action;
const flatLogicFunction = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatLogicFunctionMaps,
universalIdentifier,
});
await this.fileStorageService.move({
from: {
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction,
fileFolder: FileFolder.LogicFunctionToDelete,
}),
},
to: {
folderPath: getLogicFunctionFolderOrThrow({
flatLogicFunction,
fileFolder: FileFolder.LogicFunction,
}),
},
});
}
async rollbackForMetadata(): Promise<void> {}
}
@@ -1,20 +1,32 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type Sources } from 'twenty-shared/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { isObject } from '@sniptt/guards';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function-executor/drivers/utils/lambda-build-directory-manager';
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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { getLogicFunctionBaseFolderPath } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-base-folder-path.util';
import { UpdateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service';
@Injectable()
export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
@@ -25,6 +37,8 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
private readonly fileStorageService: FileStorageService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly functionBuildService: FunctionBuildService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
@@ -73,6 +87,41 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
this.logicFunctionExecutorService.delete(flatLogicFunction);
}
private async getApplicationUniversalIdentifier(
applicationId: string,
): Promise<string> {
const application = await this.applicationRepository.findOne({
where: { id: applicationId },
select: ['universalIdentifier'],
});
if (!isDefined(application)) {
throw new LogicFunctionException(
`Application with id ${applicationId} not found`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
return application.universalIdentifier;
}
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);
}
}
async handleChecksumUpdate({
flatLogicFunction,
code,
@@ -80,14 +129,41 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
flatLogicFunction: FlatLogicFunction;
code: Sources;
}) {
const fileFolder = getLogicFunctionFolderOrThrow({
flatLogicFunction,
});
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(
flatLogicFunction.applicationId,
);
await this.fileStorageService.writeFolder(code, fileFolder);
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
await this.writeSourcesToLocalFolder(code, sourceTemporaryDir);
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.uploadFolder_v2({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} catch (error) {
this.logger.log(
'workspace-migration-runner',
`Error updating logic function ${flatLogicFunction.id}: ${error.message}`,
);
} finally {
await lambdaBuildDirectoryManager.clean();
}
await this.functionBuildService.buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
});
}
}
@@ -1,5 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service';
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/delete-agent-action-handler.service';
@@ -70,7 +72,11 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module';
@Module({
imports: [WorkspaceSchemaManagerModule, FunctionBuildModule],
imports: [
TypeOrmModule.forFeature([ApplicationEntity]),
WorkspaceSchemaManagerModule,
FunctionBuildModule,
],
providers: [
CreateFieldActionHandlerService,
UpdateFieldActionHandlerService,