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:
+3
-1
@@ -4,9 +4,10 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
|
||||
import { WorkflowCreateManyPostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook';
|
||||
import { WorkflowCreateManyPreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-many.pre-query.hook';
|
||||
import { WorkflowCreateOnePostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook';
|
||||
@@ -41,6 +42,7 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
|
||||
RecordPositionModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ObjectMetadataModule,
|
||||
CodeStepBuildModule,
|
||||
],
|
||||
providers: [
|
||||
WorkflowCreateOnePreQueryHook,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
|
||||
import { WorkflowQueryHookModule } from 'src/modules/workflow/common/query-hooks/workflow-query-hook.module';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
|
||||
@@ -10,6 +11,7 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
|
||||
WorkflowQueryHookModule,
|
||||
LogicFunctionModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
CodeStepBuildModule,
|
||||
],
|
||||
providers: [WorkflowCommonWorkspaceService],
|
||||
exports: [WorkflowCommonWorkspaceService],
|
||||
|
||||
+2
-2
@@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
|
||||
+104
-4
@@ -1,6 +1,7 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
@@ -14,6 +15,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
|
||||
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
@@ -26,6 +28,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
let service: WorkflowVersionStepOperationsWorkspaceService;
|
||||
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
|
||||
let logicFunctionService: jest.Mocked<LogicFunctionService>;
|
||||
let applicationService: jest.Mocked<ApplicationService>;
|
||||
let codeStepBuildService: jest.Mocked<CodeStepBuildService>;
|
||||
let agentRepository: jest.Mocked<any>;
|
||||
let roleTargetRepository: jest.Mocked<any>;
|
||||
let roleRepository: jest.Mocked<any>;
|
||||
@@ -33,12 +37,53 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
|
||||
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
|
||||
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
|
||||
let flatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
applicationService = {
|
||||
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
workspaceCustomFlatApplication: {
|
||||
universalIdentifier: 'app-universal-id',
|
||||
},
|
||||
}),
|
||||
} as unknown as jest.Mocked<ApplicationService>;
|
||||
|
||||
codeStepBuildService = {
|
||||
seedCodeStepFiles: jest.fn().mockResolvedValue({
|
||||
sourceHandlerPath: 'workflow/logic-fn-id/src/index.ts',
|
||||
builtHandlerPath: 'workflow/logic-fn-id/src/index.mjs',
|
||||
checksum: 'seed-checksum',
|
||||
}),
|
||||
copySourceAndBuiltForNewCodeStep: jest.fn().mockResolvedValue(undefined),
|
||||
duplicateCodeStepLogicFunction: jest.fn().mockResolvedValue({
|
||||
id: 'new-function-id',
|
||||
name: 'Test Function',
|
||||
description: 'Test Description',
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
runtime: LogicFunctionRuntime.NODE22,
|
||||
timeoutSeconds: 30,
|
||||
sourceHandlerPath: 'src/index.ts',
|
||||
builtHandlerPath: 'index.mjs',
|
||||
handlerName: 'main',
|
||||
checksum: null,
|
||||
toolInputSchema: null,
|
||||
isTool: false,
|
||||
universalIdentifier: 'universal-id',
|
||||
applicationId: 'application-id',
|
||||
cronTriggerSettings: null,
|
||||
databaseEventTriggerSettings: null,
|
||||
httpRouteTriggerSettings: null,
|
||||
}),
|
||||
} as unknown as jest.Mocked<CodeStepBuildService>;
|
||||
|
||||
logicFunctionService = {
|
||||
createOne: jest.fn(),
|
||||
destroyOne: jest.fn(),
|
||||
duplicateLogicFunction: jest.fn(),
|
||||
} as unknown as jest.Mocked<LogicFunctionService>;
|
||||
|
||||
agentRepository = {
|
||||
@@ -86,6 +131,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
provide: LogicFunctionService,
|
||||
useValue: logicFunctionService,
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: applicationService,
|
||||
},
|
||||
{
|
||||
provide: CodeStepBuildService,
|
||||
useValue: codeStepBuildService,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AgentEntity),
|
||||
useValue: agentRepository,
|
||||
@@ -116,12 +169,12 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
},
|
||||
{
|
||||
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
useValue: {
|
||||
useValue: (flatEntityMapsCacheService = {
|
||||
flushFlatEntityMaps: jest.fn(),
|
||||
getOrRecomputeManyOrAllFlatEntityMaps: jest
|
||||
.fn()
|
||||
.mockResolvedValue(createEmptyAllFlatEntityMaps()),
|
||||
},
|
||||
} as unknown as jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
@@ -310,6 +363,29 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
nextStepIds: ['next-step'],
|
||||
} as unknown as WorkflowAction;
|
||||
|
||||
const mockExistingFlatLogicFunction: FlatLogicFunction = {
|
||||
id: 'function-id',
|
||||
name: 'Existing Function',
|
||||
description: 'Existing Description',
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
runtime: LogicFunctionRuntime.NODE22,
|
||||
timeoutSeconds: 30,
|
||||
sourceHandlerPath: 'workflow/function-id/src/index.ts',
|
||||
builtHandlerPath: 'workflow/function-id/src/index.mjs',
|
||||
handlerName: 'main',
|
||||
checksum: 'existing-checksum',
|
||||
toolInputSchema: null,
|
||||
isTool: false,
|
||||
universalIdentifier: 'existing-universal-id',
|
||||
applicationId: 'application-id',
|
||||
cronTriggerSettings: null,
|
||||
databaseEventTriggerSettings: null,
|
||||
httpRouteTriggerSettings: null,
|
||||
};
|
||||
|
||||
const mockNewFlatLogicFunction: FlatLogicFunction = {
|
||||
id: 'new-function-id',
|
||||
name: 'Test Function',
|
||||
@@ -333,7 +409,25 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
httpRouteTriggerSettings: null,
|
||||
};
|
||||
|
||||
logicFunctionService.duplicateLogicFunction.mockResolvedValue(
|
||||
const emptyMaps = createEmptyAllFlatEntityMaps();
|
||||
const flatLogicFunctionMapsKey = 'flatLogicFunctionMaps' as const;
|
||||
|
||||
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValueOnce(
|
||||
{
|
||||
...emptyMaps,
|
||||
[flatLogicFunctionMapsKey]: {
|
||||
byUniversalIdentifier: {
|
||||
'existing-universal-id': mockExistingFlatLogicFunction,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
'function-id': 'existing-universal-id',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
logicFunctionService.createOne.mockResolvedValue(
|
||||
mockNewFlatLogicFunction,
|
||||
);
|
||||
|
||||
@@ -358,6 +452,12 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
expect(codeResult.settings.input.logicFunctionId).toBe('new-function-id');
|
||||
|
||||
expect(duplicateStep.nextStepIds).toEqual([]);
|
||||
expect(
|
||||
codeStepBuildService.duplicateCodeStepLogicFunction,
|
||||
).toHaveBeenCalledWith({
|
||||
existingLogicFunctionId: 'function-id',
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should duplicate non-code step', async () => {
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
|
||||
import { CodeStepBuildService } from './services/code-step-build.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
LogicFunctionModule,
|
||||
],
|
||||
providers: [CodeStepBuildService],
|
||||
exports: [CodeStepBuildService],
|
||||
})
|
||||
export class CodeStepBuildModule {}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const CODE_STEP_DEFAULT_INPUT_SCHEMA = {
|
||||
a: null,
|
||||
b: null,
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
var main = async (params) => {
|
||||
const { a, b } = params;
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
return { message };
|
||||
};
|
||||
export {
|
||||
main
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export const main = async (params: {
|
||||
a: string;
|
||||
b: number;
|
||||
}): Promise<object> => {
|
||||
const { a, b } = params;
|
||||
|
||||
// Rename the parameters and code below with your own logic
|
||||
// This is just an example
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
|
||||
return { message };
|
||||
};
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { LogicFunctionSourceBuilderService } from 'src/engine/core-modules/logic-function/logic-function-source-builder/logic-function-source-builder.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
|
||||
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 { fromCreateLogicFunctionInputToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-input-to-flat-logic-function.util';
|
||||
import {
|
||||
WorkflowActionType,
|
||||
type WorkflowAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
const WORKFLOW_BASE_FOLDER_PREFIX = 'workflow';
|
||||
|
||||
@Injectable()
|
||||
export class CodeStepBuildService {
|
||||
constructor(
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly logicFunctionService: LogicFunctionService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly logicFunctionSourceBuilderService: LogicFunctionSourceBuilderService,
|
||||
) {}
|
||||
|
||||
async duplicateCodeStepLogicFunction({
|
||||
existingLogicFunctionId,
|
||||
workspaceId,
|
||||
}: {
|
||||
existingLogicFunctionId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<FlatLogicFunction> {
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatLogicFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingLogicFunction = findFlatLogicFunctionOrThrow({
|
||||
id: existingLogicFunctionId,
|
||||
flatLogicFunctionMaps,
|
||||
});
|
||||
|
||||
const resolvedOwnerFlatApplication = (
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
)
|
||||
).workspaceCustomFlatApplication;
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
resolvedOwnerFlatApplication.universalIdentifier;
|
||||
|
||||
const newId = v4();
|
||||
const newFlatLogicFunction =
|
||||
fromCreateLogicFunctionInputToFlatLogicFunction({
|
||||
createLogicFunctionInput: {
|
||||
name: existingLogicFunction.name,
|
||||
description: existingLogicFunction.description ?? undefined,
|
||||
timeoutSeconds: existingLogicFunction.timeoutSeconds,
|
||||
id: newId,
|
||||
},
|
||||
workspaceId,
|
||||
ownerFlatApplication: resolvedOwnerFlatApplication,
|
||||
});
|
||||
|
||||
await this.logicFunctionSourceBuilderService.copySourceAndBuilt({
|
||||
fromSourceHandlerPath: existingLogicFunction.sourceHandlerPath,
|
||||
fromBuiltHandlerPath: existingLogicFunction.builtHandlerPath,
|
||||
toSourceHandlerPath: newFlatLogicFunction.sourceHandlerPath,
|
||||
toBuiltHandlerPath: newFlatLogicFunction.builtHandlerPath,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const created = await this.logicFunctionService.createOne({
|
||||
input: {
|
||||
name: existingLogicFunction.name,
|
||||
description: existingLogicFunction.description ?? undefined,
|
||||
timeoutSeconds: existingLogicFunction.timeoutSeconds,
|
||||
id: newFlatLogicFunction.id,
|
||||
sourceHandlerPath: newFlatLogicFunction.sourceHandlerPath,
|
||||
builtHandlerPath: newFlatLogicFunction.builtHandlerPath,
|
||||
checksum: existingLogicFunction.checksum ?? undefined,
|
||||
},
|
||||
workspaceId,
|
||||
ownerFlatApplication: resolvedOwnerFlatApplication,
|
||||
});
|
||||
|
||||
if (!isDefined(created)) {
|
||||
throw new Error(
|
||||
'Failed to create logic function when duplicating code step',
|
||||
);
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async buildCodeStepsFromSourceForSteps({
|
||||
workspaceId,
|
||||
steps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
steps: WorkflowAction[];
|
||||
}): Promise<void> {
|
||||
const codeSteps = steps.filter(
|
||||
(
|
||||
step,
|
||||
): step is WorkflowAction & {
|
||||
type: typeof WorkflowActionType.CODE;
|
||||
settings: { input: { logicFunctionId: string } };
|
||||
} =>
|
||||
step.type === WorkflowActionType.CODE &&
|
||||
isDefined(
|
||||
(step.settings?.input as { logicFunctionId?: string })
|
||||
?.logicFunctionId,
|
||||
),
|
||||
);
|
||||
|
||||
if (codeSteps.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatLogicFunctionMaps, flatApplicationMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatLogicFunctionMaps', 'flatApplicationMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
for (const step of codeSteps) {
|
||||
const logicFunctionId = step.settings.input.logicFunctionId;
|
||||
const flatLogicFunction = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: logicFunctionId,
|
||||
flatEntityMaps: flatLogicFunctionMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(flatLogicFunction) ||
|
||||
flatLogicFunction.deletedAt ||
|
||||
!this.isWorkflowCodeStepLogicFunction(flatLogicFunction)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const applicationUniversalIdentifier = isDefined(
|
||||
flatLogicFunction.applicationId,
|
||||
)
|
||||
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
|
||||
?.universalIdentifier
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(applicationUniversalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { checksum } =
|
||||
await this.logicFunctionSourceBuilderService.buildFromSource({
|
||||
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
|
||||
builtHandlerPath: flatLogicFunction.builtHandlerPath,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
await this.logicFunctionService.updateChecksum({
|
||||
id: flatLogicFunction.id,
|
||||
checksum,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isWorkflowCodeStepLogicFunction(
|
||||
flatLogicFunction: FlatLogicFunction,
|
||||
): boolean {
|
||||
return (
|
||||
flatLogicFunction.sourceHandlerPath.startsWith(
|
||||
`${WORKFLOW_BASE_FOLDER_PREFIX}/`,
|
||||
) ||
|
||||
flatLogicFunction.builtHandlerPath.startsWith(
|
||||
`${WORKFLOW_BASE_FOLDER_PREFIX}/`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async getFlatLogicFunctionForCodeStepOrNull({
|
||||
logicFunctionId,
|
||||
workspaceId,
|
||||
}: {
|
||||
logicFunctionId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<FlatLogicFunction | null> {
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatLogicFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatLogicFunction = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: logicFunctionId,
|
||||
flatEntityMaps: flatLogicFunctionMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(flatLogicFunction) ||
|
||||
flatLogicFunction.deletedAt ||
|
||||
!this.isWorkflowCodeStepLogicFunction(flatLogicFunction)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return flatLogicFunction;
|
||||
}
|
||||
}
|
||||
+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}/`, '');
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||
|
||||
export type CodeStepSeedProjectFile = {
|
||||
name: string;
|
||||
path: string;
|
||||
content: Buffer;
|
||||
};
|
||||
|
||||
const getAllFiles = async (
|
||||
rootDir: string,
|
||||
dir: string = rootDir,
|
||||
files: CodeStepSeedProjectFile[] = [],
|
||||
): Promise<CodeStepSeedProjectFile[]> => {
|
||||
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of dirEntries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await getAllFiles(rootDir, fullPath, files);
|
||||
} else {
|
||||
files.push({
|
||||
path: path.relative(rootDir, dir),
|
||||
name: entry.name,
|
||||
content: await fs.readFile(fullPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
export const getCodeStepSeedProjectFiles = async (): Promise<
|
||||
CodeStepSeedProjectFile[]
|
||||
> => {
|
||||
const seedProjectPath = path.join(
|
||||
ASSET_PATH,
|
||||
'modules/workflow/workflow-builder/workflow-version-step/code-step/constants/seed-project',
|
||||
);
|
||||
|
||||
return getAllFiles(seedProjectPath);
|
||||
};
|
||||
+16
-10
@@ -15,7 +15,7 @@ import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { SEED_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/seed-project-input-schema';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
@@ -28,6 +28,8 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { CODE_STEP_DEFAULT_INPUT_SCHEMA } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/constants/seed-project/code-step-default-input-schema';
|
||||
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
|
||||
import {
|
||||
WorkflowVersionStepException,
|
||||
WorkflowVersionStepExceptionCode,
|
||||
@@ -65,6 +67,8 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly logicFunctionService: LogicFunctionService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly codeStepBuildService: CodeStepBuildService,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(RoleTargetEntity)
|
||||
@@ -151,8 +155,12 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
|
||||
switch (type) {
|
||||
case WorkflowActionType.CODE: {
|
||||
const logicFunctionId = id ?? v4();
|
||||
|
||||
// createOne handles seeding source files with default seed project
|
||||
const newLogicFunction = await this.logicFunctionService.createOne({
|
||||
input: {
|
||||
id: logicFunctionId,
|
||||
name: 'A Logic Function Code Workflow Step',
|
||||
description: '',
|
||||
},
|
||||
@@ -184,7 +192,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
},
|
||||
input: {
|
||||
logicFunctionId: newLogicFunction.id,
|
||||
logicFunctionInput: SEED_PROJECT_INPUT_SCHEMA,
|
||||
logicFunctionInput: CODE_STEP_DEFAULT_INPUT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -655,8 +663,8 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
switch (step.type) {
|
||||
case WorkflowActionType.CODE: {
|
||||
const newLogicFunction =
|
||||
await this.logicFunctionService.duplicateLogicFunction({
|
||||
id: step.settings.input.logicFunctionId,
|
||||
await this.codeStepBuildService.duplicateCodeStepLogicFunction({
|
||||
existingLogicFunctionId: step.settings.input.logicFunctionId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
@@ -928,12 +936,10 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
switch (step.type) {
|
||||
case WorkflowActionType.CODE: {
|
||||
const newLogicFunction =
|
||||
await this.logicFunctionService.createLogicFunctionFromExistingLogicFunctionById(
|
||||
{
|
||||
id: step.settings.input.logicFunctionId,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
await this.codeStepBuildService.duplicateCodeStepLogicFunction({
|
||||
existingLogicFunctionId: step.settings.input.logicFunctionId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...step,
|
||||
|
||||
+5
@@ -2,10 +2,12 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
@@ -24,6 +26,8 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
|
||||
WorkflowSchemaModule,
|
||||
LogicFunctionModule,
|
||||
WorkflowCommonModule,
|
||||
ApplicationModule,
|
||||
CodeStepBuildModule,
|
||||
AiAgentRoleModule,
|
||||
WorkspaceCacheModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([
|
||||
@@ -46,6 +50,7 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
|
||||
WorkflowVersionStepWorkspaceService,
|
||||
WorkflowVersionStepOperationsWorkspaceService,
|
||||
WorkflowVersionStepHelpersWorkspaceService,
|
||||
CodeStepBuildModule,
|
||||
],
|
||||
})
|
||||
export class WorkflowVersionStepModule {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
|
||||
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
|
||||
import { RUN_WORKFLOW_JOB_NAME } from 'src/modules/workflow/workflow-runner/constants/run-workflow-job-name';
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types
|
||||
export class RunWorkflowJob {
|
||||
constructor(
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly codeStepBuildService: CodeStepBuildService,
|
||||
private readonly workflowExecutorWorkspaceService: WorkflowExecutorWorkspaceService,
|
||||
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
||||
private readonly metricsService: MetricsService,
|
||||
@@ -97,6 +99,11 @@ export class RunWorkflowJob {
|
||||
);
|
||||
}
|
||||
|
||||
await this.codeStepBuildService.buildCodeStepsFromSourceForSteps({
|
||||
workspaceId,
|
||||
steps: workflowVersion.steps,
|
||||
});
|
||||
|
||||
await this.workflowRunWorkspaceService.startWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
|
||||
+4
@@ -4,8 +4,10 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
|
||||
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
|
||||
import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module';
|
||||
import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
|
||||
@@ -14,11 +16,13 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t
|
||||
@Module({
|
||||
imports: [
|
||||
WorkflowCommonModule,
|
||||
CodeStepBuildModule,
|
||||
WorkflowRunnerModule,
|
||||
AutomatedTriggerModule,
|
||||
CommandMenuItemModule,
|
||||
FeatureFlagModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
|
||||
LogicFunctionModule,
|
||||
],
|
||||
providers: [WorkflowTriggerWorkspaceService, WorkflowTriggerJob],
|
||||
exports: [WorkflowTriggerWorkspaceService],
|
||||
|
||||
+11
-1
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { assertWorkflowVersionTriggerIsDefined } from 'src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { CodeStepBuildService } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service';
|
||||
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
|
||||
import { WORKFLOW_VERSION_STATUS_UPDATED } from 'src/modules/workflow/workflow-status/constants/workflow-version-status-updated.constants';
|
||||
import { type WorkflowVersionStatusUpdate } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
|
||||
@@ -41,6 +43,7 @@ export class WorkflowTriggerWorkspaceService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly codeStepBuildService: CodeStepBuildService,
|
||||
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
|
||||
private readonly automatedTriggerWorkspaceService: AutomatedTriggerWorkspaceService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
@@ -121,6 +124,11 @@ export class WorkflowTriggerWorkspaceService {
|
||||
|
||||
assertVersionCanBeActivated(workflowVersion, workflow);
|
||||
|
||||
await this.codeStepBuildService.buildCodeStepsFromSourceForSteps({
|
||||
workspaceId,
|
||||
steps: workflowVersion.steps ?? [],
|
||||
});
|
||||
|
||||
await this.performActivationSteps(
|
||||
workflow,
|
||||
workflowVersion,
|
||||
@@ -376,7 +384,9 @@ export class WorkflowTriggerWorkspaceService {
|
||||
await this.commandMenuItemService.create(
|
||||
{
|
||||
workflowVersionId: workflowVersion.id,
|
||||
label: workflow.name ?? 'Manual Trigger',
|
||||
label: isNonEmptyString(workflow.name)
|
||||
? workflow.name
|
||||
: 'Manual Trigger',
|
||||
icon: trigger.settings.icon,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
|
||||
Reference in New Issue
Block a user