Refactor global datasource part 3 (#16447)

## Context
Following https://github.com/twentyhq/twenty/pull/16399
Now using the new global orm manager everywhere and returning a
GlobalDatasource/WorkspaceDatasource based on a feature flag.
This means we now need to wrap all our ORM calls within
executeInWorkspaceContext callback (at least for now) so the global
datasource can dynamically hydrate its context via the new store (the
global datasource does not store anything related to workspaces as it is
now a unique singleton). If feature flag is off it still uses local data
stored in the workspace datasource.
This commit is contained in:
Weiko
2025-12-10 17:17:33 +01:00
committed by GitHub
parent 4f13022774
commit 9bd8f94b3a
203 changed files with 8887 additions and 7237 deletions
@@ -1,18 +1,19 @@
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
WorkflowVersionStatus,
type WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@WorkspaceQueryHook({
key: `workflow.createMany`,
@@ -22,7 +23,7 @@ export class WorkflowCreateManyPostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordPositionService: RecordPositionService,
) {}
@@ -35,32 +36,37 @@ export class WorkflowCreateManyPostQueryHook
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspace.id,
'workflowVersion',
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext as WorkspaceAuthContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspace.id,
'workflowVersion',
);
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId: workspace.id,
});
const workflowVersionsToCreate = payload.map((workflow) => ({
workflowId: workflow.id,
status: WorkflowVersionStatus.DRAFT,
name: 'v1',
position,
}));
await Promise.all(
workflowVersionsToCreate.map((workflowVersion) => {
return workflowVersionRepository.insert(workflowVersion);
}),
);
},
workspaceId: workspace.id,
});
const workflowVersionsToCreate = payload.map((workflow) => ({
workflowId: workflow.id,
status: WorkflowVersionStatus.DRAFT,
name: 'v1',
position,
}));
await Promise.all(
workflowVersionsToCreate.map((workflowVersion) => {
return workflowVersionRepository.insert(workflowVersion);
}),
);
}
}
@@ -1,18 +1,19 @@
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
WorkflowVersionStatus,
type WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@WorkspaceQueryHook({
key: `workflow.createOne`,
@@ -22,7 +23,7 @@ export class WorkflowCreateOnePostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordPositionService: RecordPositionService,
) {}
@@ -37,26 +38,31 @@ export class WorkflowCreateOnePostQueryHook
const workflow = payload[0];
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspace.id,
'workflowVersion',
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext as WorkspaceAuthContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspace.id,
'workflowVersion',
);
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId: workspace.id,
});
await workflowVersionRepository.insert({
workflowId: workflow.id,
status: WorkflowVersionStatus.DRAFT,
name: 'v1',
position,
});
},
workspaceId: workspace.id,
});
await workflowVersionRepository.insert({
workflowId: workflow.id,
status: WorkflowVersionStatus.DRAFT,
name: 'v1',
position,
});
);
}
}
@@ -2,14 +2,15 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
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 { 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 { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-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';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import {
WorkflowCommonException,
WorkflowCommonExceptionCode,
@@ -39,7 +40,7 @@ export type ObjectMetadataInfo = {
@Injectable()
export class WorkflowCommonWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -58,20 +59,27 @@ export class WorkflowCommonWorkspaceService {
);
}
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
return this.getValidWorkflowVersionOrFail(workflowVersion);
},
});
return this.getValidWorkflowVersionOrFail(workflowVersion);
);
}
async getValidWorkflowVersionOrFail(
@@ -84,14 +92,6 @@ export class WorkflowCommonWorkspaceService {
);
}
// FIXME: For now we will make the trigger optional. Later, we'll have to ensure the trigger is defined when publishing the flow.
// if (!workflowVersion.trigger) {
// throw new WorkflowTriggerException(
// 'Workflow version does not contains trigger',
// WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
// );
// }
return { ...workflowVersion, trigger: workflowVersion.trigger };
}
@@ -163,73 +163,80 @@ export class WorkflowCommonWorkspaceService {
workspaceId: string;
operation: 'restore' | 'delete' | 'destroy';
}): Promise<void> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowAutomatedTriggerRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
'workflowAutomatedTrigger',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
for (const workflowId of workflowIds) {
switch (operation) {
case 'delete':
await workflowAutomatedTriggerRepository.softDelete({
const workflowAutomatedTriggerRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
'workflowAutomatedTrigger',
{ shouldBypassPermissionChecks: true },
);
for (const workflowId of workflowIds) {
switch (operation) {
case 'delete':
await workflowAutomatedTriggerRepository.softDelete({
workflowId,
});
await workflowRunRepository.softDelete({
workflowId,
});
await workflowVersionRepository.softDelete({
workflowId,
});
break;
case 'restore':
await workflowAutomatedTriggerRepository.restore({
workflowId,
});
await workflowRunRepository.restore({
workflowId,
});
await workflowVersionRepository.restore({
workflowId,
});
break;
}
await this.deactivateVersionOnDelete({
workflowVersionRepository,
workflowId,
workspaceId,
operation,
});
await workflowRunRepository.softDelete({
await this.handleServerlessFunctionSubEntities({
workflowVersionRepository,
workflowId,
workspaceId,
operation,
});
await workflowVersionRepository.softDelete({
workflowId,
});
break;
case 'restore':
await workflowAutomatedTriggerRepository.restore({
workflowId,
});
await workflowRunRepository.restore({
workflowId,
});
await workflowVersionRepository.restore({
workflowId,
});
break;
}
await this.deactivateVersionOnDelete({
workflowVersionRepository,
workflowId,
workspaceId,
operation,
});
await this.handleServerlessFunctionSubEntities({
workflowVersionRepository,
workflowId,
workspaceId,
operation,
});
}
}
},
);
}
private async deactivateVersionOnDelete({
@@ -248,10 +255,10 @@ export class WorkflowCommonWorkspaceService {
}
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
{ shouldBypassPermissionChecks: true },
);
const workflow = await workflowRepository.findOne({
@@ -9,7 +9,8 @@ import {
type UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowQueryValidationException,
WorkflowQueryValidationExceptionCode,
@@ -25,7 +26,7 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
export class WorkflowVersionValidationWorkspaceService {
constructor(
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async validateWorkflowVersionForCreateOne(
@@ -45,32 +46,38 @@ export class WorkflowVersionValidationWorkspaceService {
);
}
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowAlreadyHasDraftVersion =
await workflowVersionRepository.exists({
where: {
workflowId: payload.data.workflowId,
status: WorkflowVersionStatus.DRAFT,
// FIXME: soft-deleted rows selection will have to be improved globally
deletedAt: IsNull(),
},
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
if (workflowAlreadyHasDraftVersion) {
throw new WorkflowQueryValidationException(
'Cannot create multiple draft versions for the same workflow',
WorkflowQueryValidationExceptionCode.FORBIDDEN,
{
userFriendlyMessage: msg`Cannot create multiple draft versions for the same workflow`,
},
);
}
const workflowAlreadyHasDraftVersion =
await workflowVersionRepository.exists({
where: {
workflowId: payload.data.workflowId,
status: WorkflowVersionStatus.DRAFT,
deletedAt: IsNull(),
},
});
if (workflowAlreadyHasDraftVersion) {
throw new WorkflowQueryValidationException(
'Cannot create multiple draft versions for the same workflow',
WorkflowQueryValidationExceptionCode.FORBIDDEN,
{
userFriendlyMessage: msg`Cannot create multiple draft versions for the same workflow`,
},
);
}
},
);
}
async validateWorkflowVersionForUpdateOne({
@@ -86,8 +93,6 @@ export class WorkflowVersionValidationWorkspaceService {
workflowVersionId: payload.id,
});
// If the only field updated is the name, we can update the workflow version
// Otherwise, we need to assert that the workflow version is a draft
if (!(Object.keys(payload.data).length === 1 && payload.data.name)) {
assertWorkflowVersionIsDraft(workflowVersion);
}
@@ -123,29 +128,37 @@ export class WorkflowVersionValidationWorkspaceService {
assertWorkflowVersionIsDraft(workflowVersion);
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const authContext = buildSystemAuthContext(workspaceId);
const otherWorkflowVersionsExist = await workflowVersionRepository.exists({
where: {
workflowId: workflowVersion.workflowId,
deletedAt: IsNull(),
id: Not(workflowVersion.id),
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const otherWorkflowVersionsExist =
await workflowVersionRepository.exists({
where: {
workflowId: workflowVersion.workflowId,
deletedAt: IsNull(),
id: Not(workflowVersion.id),
},
});
if (!otherWorkflowVersionsExist) {
throw new WorkflowQueryValidationException(
'The initial version of a workflow can not be deleted',
WorkflowQueryValidationExceptionCode.FORBIDDEN,
{
userFriendlyMessage: msg`The initial version of a workflow can not be deleted`,
},
);
}
},
});
if (!otherWorkflowVersionsExist) {
throw new WorkflowQueryValidationException(
'The initial version of a workflow can not be deleted',
WorkflowQueryValidationExceptionCode.FORBIDDEN,
{
userFriendlyMessage: msg`The initial version of a workflow can not be deleted`,
},
);
}
);
}
}
@@ -2,8 +2,8 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
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';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
@@ -73,7 +73,7 @@ const mockWorkflowVersion = {
} as WorkflowVersionWorkspaceEntity;
describe('WorkflowVersionEdgeWorkspaceService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
let service: WorkflowVersionEdgeWorkspaceService;
let mockWorkflowVersionWorkspaceRepository: MockWorkspaceRepository;
@@ -88,11 +88,14 @@ describe('WorkflowVersionEdgeWorkspaceService', () => {
mockWorkflowVersion,
);
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest
globalWorkspaceOrmManager = {
executeInWorkspaceContext: jest
.fn()
.mockImplementation(async (_authContext, callback) => callback()),
getRepository: jest
.fn()
.mockResolvedValue(mockWorkflowVersionWorkspaceRepository),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
} as unknown as jest.Mocked<GlobalWorkspaceOrmManager>;
workflowCommonWorkspaceService = {
getWorkflowVersionOrFail: jest
@@ -104,8 +107,8 @@ describe('WorkflowVersionEdgeWorkspaceService', () => {
providers: [
WorkflowVersionEdgeWorkspaceService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
{
provide: WorkflowCommonWorkspaceService,
@@ -4,8 +4,9 @@ import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { type WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import {
WorkflowVersionEdgeException,
WorkflowVersionEdgeExceptionCode,
@@ -24,7 +25,7 @@ import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/type
@Injectable()
export class WorkflowVersionEdgeWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
@@ -41,54 +42,61 @@ export class WorkflowVersionEdgeWorkspaceService {
workspaceId: string;
sourceConnectionOptions?: WorkflowStepConnectionOptions;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
assertWorkflowVersionIsDraft(workflowVersion);
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
});
const trigger = workflowVersion.trigger;
const steps = workflowVersion.steps || [];
assertWorkflowVersionIsDraft(workflowVersion);
const targetStep = steps.find((step) => step.id === target);
const trigger = workflowVersion.trigger;
const steps = workflowVersion.steps || [];
if (!isDefined(targetStep)) {
throw new WorkflowVersionEdgeException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
const targetStep = steps.find((step) => step.id === target);
const isSourceTrigger = source === TRIGGER_STEP_ID;
if (!isDefined(targetStep)) {
throw new WorkflowVersionEdgeException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (isSourceTrigger) {
return this.createTriggerEdge({
trigger,
steps,
target,
workflowVersion,
workflowVersionRepository,
});
} else {
return this.createStepEdge({
trigger,
steps,
source,
target,
sourceConnectionOptions,
workflowVersion,
workflowVersionRepository,
});
}
const isSourceTrigger = source === TRIGGER_STEP_ID;
if (isSourceTrigger) {
return this.createTriggerEdge({
trigger,
steps,
target,
workflowVersion,
workflowVersionRepository,
});
} else {
return this.createStepEdge({
trigger,
steps,
source,
target,
sourceConnectionOptions,
workflowVersion,
workflowVersionRepository,
});
}
},
);
}
async deleteWorkflowVersionEdge({
@@ -104,54 +112,61 @@ export class WorkflowVersionEdgeWorkspaceService {
workspaceId: string;
sourceConnectionOptions?: WorkflowStepConnectionOptions;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
assertWorkflowVersionIsDraft(workflowVersion);
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
});
const trigger = workflowVersion.trigger;
const steps = workflowVersion.steps || [];
assertWorkflowVersionIsDraft(workflowVersion);
const targetStep = steps.find((step) => step.id === target);
const trigger = workflowVersion.trigger;
const steps = workflowVersion.steps || [];
if (!isDefined(targetStep)) {
throw new WorkflowVersionEdgeException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
const targetStep = steps.find((step) => step.id === target);
const isSourceTrigger = source === TRIGGER_STEP_ID;
if (!isDefined(targetStep)) {
throw new WorkflowVersionEdgeException(
`Target step '${target}' not found in workflowVersion '${workflowVersionId}'`,
WorkflowVersionEdgeExceptionCode.NOT_FOUND,
);
}
if (isSourceTrigger) {
return this.deleteTriggerEdge({
trigger,
steps,
target,
workflowVersion,
workflowVersionRepository,
});
} else {
return this.deleteStepEdge({
trigger,
steps,
source,
target,
workflowVersion,
workflowVersionRepository,
sourceConnectionOptions,
});
}
const isSourceTrigger = source === TRIGGER_STEP_ID;
if (isSourceTrigger) {
return this.deleteTriggerEdge({
trigger,
steps,
target,
workflowVersion,
workflowVersionRepository,
});
} else {
return this.deleteStepEdge({
trigger,
steps,
source,
target,
workflowVersion,
workflowVersionRepository,
sourceConnectionOptions,
});
}
},
);
}
private async createTriggerEdge({
@@ -8,7 +8,7 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
@@ -21,7 +21,7 @@ const mockWorkspaceId = 'workspace-id';
describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let service: WorkflowVersionStepOperationsWorkspaceService;
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let serverlessFunctionService: jest.Mocked<ServerlessFunctionService>;
let agentRepository: jest.Mocked<any>;
let roleTargetRepository: jest.Mocked<any>;
@@ -67,20 +67,19 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
deleteAgentOnlyRoleIfUnused: jest.fn(),
} as unknown as jest.Mocked<AiAgentRoleService>;
globalWorkspaceOrmManager = {
getRepository: jest.fn(),
} as unknown as jest.Mocked<GlobalWorkspaceOrmManager>;
workspaceCacheService = {
flush: jest.fn(),
} as unknown as jest.Mocked<WorkspaceCacheService>;
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn(),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkflowVersionStepOperationsWorkspaceService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
{
provide: ServerlessFunctionService,
@@ -2,17 +2,17 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
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';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import { WorkflowVersionStepHelpersWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service';
import { WorkflowVersionStepCreationWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-creation.workspace-service';
import { WorkflowVersionStepUpdateWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-update.workspace-service';
import { WorkflowVersionStepDeletionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-deletion.workspace-service';
import { WorkflowVersionStepHelpersWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import { WorkflowVersionStepUpdateWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-update.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import {
type WorkflowAction,
WorkflowActionType,
@@ -86,7 +86,7 @@ const mockWorkflowVersion = {
} as WorkflowVersionWorkspaceEntity;
describe('WorkflowVersionStepWorkspaceService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let service: WorkflowVersionStepWorkspaceService;
let mockWorkflowVersionWorkspaceRepository: MockWorkspaceRepository;
let mockComputeWorkflowVersionStepChanges: jest.Mock;
@@ -108,11 +108,15 @@ describe('WorkflowVersionStepWorkspaceService', () => {
mockWorkflowVersion,
);
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest
globalWorkspaceOrmManager = {
getRepository: jest
.fn()
.mockResolvedValue(mockWorkflowVersionWorkspaceRepository),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
} as unknown as jest.Mocked<GlobalWorkspaceOrmManager>;
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -122,8 +126,8 @@ describe('WorkflowVersionStepWorkspaceService', () => {
WorkflowVersionStepUpdateWorkspaceService,
WorkflowVersionStepDeletionWorkspaceService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
{
provide: WorkflowSchemaWorkspaceService,
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@@ -10,7 +11,7 @@ import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/type
@Injectable()
export class WorkflowVersionStepHelpersWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
@@ -43,23 +44,30 @@ export class WorkflowVersionStepHelpersWorkspaceService {
steps?: WorkflowAction[] | null;
trigger?: WorkflowTrigger | null;
}): Promise<void> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const updateData: Partial<WorkflowVersionWorkspaceEntity> = {};
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
if (steps !== undefined) {
updateData.steps = steps;
}
const updateData: Partial<WorkflowVersionWorkspaceEntity> = {};
if (trigger !== undefined) {
updateData.trigger = trigger;
}
if (steps !== undefined) {
updateData.steps = steps;
}
await workflowVersionRepository.update(workflowVersionId, updateData);
if (trigger !== undefined) {
updateData.trigger = trigger;
}
await workflowVersionRepository.update(workflowVersionId, updateData);
},
);
}
}
@@ -15,7 +15,8 @@ import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/co
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 { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowVersionStepException,
@@ -52,7 +53,7 @@ const ITERATOR_EMPTY_STEP_POSITION_OFFSET = {
@Injectable()
export class WorkflowVersionStepOperationsWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly serverlessFunctionService: ServerlessFunctionService,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@@ -359,7 +360,6 @@ export class WorkflowVersionStepOperationsWorkspaceService {
};
}
case WorkflowActionType.AI_AGENT: {
// Get workflow version to use workflow ID and name in agent name
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
@@ -465,66 +465,75 @@ export class WorkflowVersionStepOperationsWorkspaceService {
step: WorkflowFormAction;
response: object;
}) {
const responseKeys = Object.keys(response);
const authContext = buildSystemAuthContext(workspaceId);
const enrichedResponses = await Promise.all(
responseKeys.map(async (key) => {
// @ts-expect-error legacy noImplicitAny
if (!isDefined(response[key])) {
// @ts-expect-error legacy noImplicitAny
return { key, value: response[key] };
}
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const responseKeys = Object.keys(response);
const field = step.settings.input.find((field) => field.name === key);
if (
field?.type === 'RECORD' &&
field?.settings?.objectName &&
// @ts-expect-error legacy noImplicitAny
isDefined(response[key].id) &&
// @ts-expect-error legacy noImplicitAny
isValidUuid(response[key].id)
) {
const { flatObjectMetadata, flatFieldMetadataMaps } =
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
field.settings.objectName,
workspaceId,
);
const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
)
.filter((field) => field.type === FieldMetadataType.RELATION)
.map((field) => field.name);
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
field.settings.objectName,
{ shouldBypassPermissionChecks: true },
);
const record = await repository.findOne({
const enrichedResponses = await Promise.all(
responseKeys.map(async (key) => {
// @ts-expect-error legacy noImplicitAny
where: { id: response[key].id },
relations: relationFieldsNames,
});
if (!isDefined(response[key])) {
// @ts-expect-error legacy noImplicitAny
return { key, value: response[key] };
}
return { key, value: record };
} else {
const field = step.settings.input.find(
(field) => field.name === key,
);
if (
field?.type === 'RECORD' &&
field?.settings?.objectName &&
// @ts-expect-error legacy noImplicitAny
isDefined(response[key].id) &&
// @ts-expect-error legacy noImplicitAny
isValidUuid(response[key].id)
) {
const { flatObjectMetadata, flatFieldMetadataMaps } =
await this.workflowCommonWorkspaceService.getObjectMetadataInfo(
field.settings.objectName,
workspaceId,
);
const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
)
.filter((field) => field.type === FieldMetadataType.RELATION)
.map((field) => field.name);
const repository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
field.settings.objectName,
{ shouldBypassPermissionChecks: true },
);
const record = await repository.findOne({
// @ts-expect-error legacy noImplicitAny
where: { id: response[key].id },
relations: relationFieldsNames,
});
return { key, value: record };
} else {
// @ts-expect-error legacy noImplicitAny
return { key, value: response[key] };
}
}),
);
return enrichedResponses.reduce((acc, { key, value }) => {
// @ts-expect-error legacy noImplicitAny
return { key, value: response[key] };
}
}),
acc[key] = value;
return acc;
}, {});
},
);
return enrichedResponses.reduce((acc, { key, value }) => {
// @ts-expect-error legacy noImplicitAny
acc[key] = value;
return acc;
}, {});
}
async cloneStep({
@@ -650,49 +659,60 @@ export class WorkflowVersionStepOperationsWorkspaceService {
workspaceId: string;
iteratorPosition?: WorkflowStepPositionInput;
}): Promise<WorkflowAction> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
const existingSteps = workflowVersion.steps ?? [];
const emptyNodeStep: WorkflowEmptyAction = {
id: v4(),
name: 'Add an Action',
type: WorkflowActionType.EMPTY,
valid: true,
nextStepIds: [iteratorStepId],
settings: {
...BASE_STEP_DEFINITION,
input: {},
},
position: {
x:
(iteratorPosition?.x ?? 0) +
ITERATOR_EMPTY_STEP_POSITION_OFFSET.x,
y:
(iteratorPosition?.y ?? 0) +
ITERATOR_EMPTY_STEP_POSITION_OFFSET.y,
},
};
await workflowVersionRepository.update(workflowVersion.id, {
steps: [...existingSteps, emptyNodeStep],
});
return emptyNodeStep;
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
const existingSteps = workflowVersion.steps ?? [];
const emptyNodeStep: WorkflowEmptyAction = {
id: v4(),
name: 'Add an Action',
type: WorkflowActionType.EMPTY,
valid: true,
nextStepIds: [iteratorStepId],
settings: {
...BASE_STEP_DEFINITION,
input: {},
},
position: {
x: (iteratorPosition?.x ?? 0) + ITERATOR_EMPTY_STEP_POSITION_OFFSET.x,
y: (iteratorPosition?.y ?? 0) + ITERATOR_EMPTY_STEP_POSITION_OFFSET.y,
},
};
await workflowVersionRepository.update(workflowVersion.id, {
steps: [...existingSteps, emptyNodeStep],
});
return emptyNodeStep;
);
}
async createDraftStep({
@@ -5,7 +5,8 @@ import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { type WorkflowStepPositionUpdateInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-update-input.dto';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowVersionStepException,
WorkflowVersionStepExceptionCode,
@@ -31,7 +32,7 @@ import {
@Injectable()
export class WorkflowVersionWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService,
private readonly workflowVersionStepOperationsWorkspaceService: WorkflowVersionStepOperationsWorkspaceService,
private readonly recordPositionService: RecordPositionService,
@@ -46,90 +47,99 @@ export class WorkflowVersionWorkspaceService {
workflowId: string;
workflowVersionIdToCopy: string;
}) {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersionToCopy = await workflowVersionRepository.findOne({
where: {
id: workflowVersionIdToCopy,
workflowId,
},
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
if (!isDefined(workflowVersionToCopy)) {
throw new WorkflowVersionStepException(
'WorkflowVersion to copy not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionTriggerIsDefined(workflowVersionToCopy);
assertWorkflowVersionHasSteps(workflowVersionToCopy);
let draftWorkflowVersion = await workflowVersionRepository.findOne({
where: {
workflowId,
status: WorkflowVersionStatus.DRAFT,
},
});
if (!isDefined(draftWorkflowVersion)) {
const workflowVersionsCount = await workflowVersionRepository.count({
where: {
workflowId,
},
});
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const insertResult = await workflowVersionRepository.insert({
workflowId,
name: `v${workflowVersionsCount + 1}`,
status: WorkflowVersionStatus.DRAFT,
position,
});
draftWorkflowVersion = insertResult
.generatedMaps[0] as WorkflowVersionWorkspaceEntity;
}
assertWorkflowVersionIsDraft(draftWorkflowVersion);
const newWorkflowVersionTrigger = workflowVersionToCopy.trigger;
const newWorkflowVersionSteps: WorkflowAction[] = [];
for (const step of workflowVersionToCopy.steps) {
const duplicatedStep =
await this.workflowVersionStepWorkspaceService.createDraftStep({
step,
workspaceId,
const workflowVersionToCopy = await workflowVersionRepository.findOne({
where: {
id: workflowVersionIdToCopy,
workflowId,
},
});
newWorkflowVersionSteps.push(duplicatedStep);
}
if (!isDefined(workflowVersionToCopy)) {
throw new WorkflowVersionStepException(
'WorkflowVersion to copy not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
await workflowVersionRepository.update(draftWorkflowVersion.id, {
steps: newWorkflowVersionSteps,
trigger: newWorkflowVersionTrigger,
});
assertWorkflowVersionTriggerIsDefined(workflowVersionToCopy);
assertWorkflowVersionHasSteps(workflowVersionToCopy);
return {
...draftWorkflowVersion,
name: draftWorkflowVersion.name ?? '',
steps: newWorkflowVersionSteps,
trigger: newWorkflowVersionTrigger,
};
let draftWorkflowVersion = await workflowVersionRepository.findOne({
where: {
workflowId,
status: WorkflowVersionStatus.DRAFT,
},
});
if (!isDefined(draftWorkflowVersion)) {
const workflowVersionsCount = await workflowVersionRepository.count({
where: {
workflowId,
},
});
const position = await this.recordPositionService.buildRecordPosition(
{
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
},
);
const insertResult = await workflowVersionRepository.insert({
workflowId,
name: `v${workflowVersionsCount + 1}`,
status: WorkflowVersionStatus.DRAFT,
position,
});
draftWorkflowVersion = insertResult
.generatedMaps[0] as WorkflowVersionWorkspaceEntity;
}
assertWorkflowVersionIsDraft(draftWorkflowVersion);
const newWorkflowVersionTrigger = workflowVersionToCopy.trigger;
const newWorkflowVersionSteps: WorkflowAction[] = [];
for (const step of workflowVersionToCopy.steps) {
const duplicatedStep =
await this.workflowVersionStepWorkspaceService.createDraftStep({
step,
workspaceId,
});
newWorkflowVersionSteps.push(duplicatedStep);
}
await workflowVersionRepository.update(draftWorkflowVersion.id, {
steps: newWorkflowVersionSteps,
trigger: newWorkflowVersionTrigger,
});
return {
...draftWorkflowVersion,
name: draftWorkflowVersion.name ?? '',
steps: newWorkflowVersionSteps,
trigger: newWorkflowVersionTrigger,
};
},
);
}
async duplicateWorkflow({
@@ -141,159 +151,167 @@ export class WorkflowVersionWorkspaceService {
workflowIdToDuplicate: string;
workflowVersionIdToCopy: string;
}) {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const sourceWorkflow = await workflowRepository.findOne({
where: {
id: workflowIdToDuplicate,
},
});
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
if (!isDefined(sourceWorkflow)) {
throw new WorkflowVersionStepException(
'Source workflow not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
const sourceVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionIdToCopy,
workflowId: workflowIdToDuplicate,
},
});
if (!isDefined(sourceVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion to copy not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionTriggerIsDefined(sourceVersion);
assertWorkflowVersionHasSteps(sourceVersion);
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const insertWorkflowResult = await workflowRepository.insert({
name: `${sourceWorkflow.name} (Duplicate)`,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
});
const newWorkflowId = (
insertWorkflowResult.generatedMaps[0] as WorkflowWorkspaceEntity
).id;
const versionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const insertVersionResult = await workflowVersionRepository.insert({
workflowId: newWorkflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
position: versionPosition,
});
const newDraftVersion = insertVersionResult
.generatedMaps[0] as WorkflowVersionWorkspaceEntity;
const newTrigger = sourceVersion.trigger;
const sourceToClonedPairs: Array<{
source: WorkflowAction;
duplicated: WorkflowAction;
}> = [];
const oldToNewIdMap = new Map<string, string>();
for (const step of sourceVersion.steps ?? []) {
const clonedStep =
await this.workflowVersionStepOperationsWorkspaceService.cloneStep({
step,
workspaceId,
const sourceWorkflow = await workflowRepository.findOne({
where: {
id: workflowIdToDuplicate,
},
});
sourceToClonedPairs.push({
source: step,
duplicated: clonedStep,
});
oldToNewIdMap.set(step.id, clonedStep.id);
}
const remappedTrigger = isDefined(newTrigger)
? {
...newTrigger,
nextStepIds: (newTrigger.nextStepIds ?? []).map(
(oldId) => oldToNewIdMap.get(oldId) ?? oldId,
),
if (!isDefined(sourceWorkflow)) {
throw new WorkflowVersionStepException(
'Source workflow not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
: undefined;
const remappedSteps: WorkflowAction[] = sourceToClonedPairs.map(
({ source, duplicated }) => {
const remappedStep = {
...duplicated,
nextStepIds: (source.nextStepIds ?? []).map(
(oldId) => oldToNewIdMap.get(oldId) ?? oldId,
),
};
const sourceVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionIdToCopy,
workflowId: workflowIdToDuplicate,
},
});
if (
source.type === WorkflowActionType.ITERATOR &&
isDefined(source.settings?.input?.initialLoopStepIds)
) {
remappedStep.settings = {
...remappedStep.settings,
input: {
...remappedStep.settings.input,
initialLoopStepIds: source.settings.input.initialLoopStepIds.map(
if (!isDefined(sourceVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion to copy not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
assertWorkflowVersionTriggerIsDefined(sourceVersion);
assertWorkflowVersionHasSteps(sourceVersion);
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const insertWorkflowResult = await workflowRepository.insert({
name: `${sourceWorkflow.name} (Duplicate)`,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
});
const newWorkflowId = (
insertWorkflowResult.generatedMaps[0] as WorkflowWorkspaceEntity
).id;
const versionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const insertVersionResult = await workflowVersionRepository.insert({
workflowId: newWorkflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
position: versionPosition,
});
const newDraftVersion = insertVersionResult
.generatedMaps[0] as WorkflowVersionWorkspaceEntity;
const newTrigger = sourceVersion.trigger;
const sourceToClonedPairs: Array<{
source: WorkflowAction;
duplicated: WorkflowAction;
}> = [];
const oldToNewIdMap = new Map<string, string>();
for (const step of sourceVersion.steps ?? []) {
const clonedStep =
await this.workflowVersionStepOperationsWorkspaceService.cloneStep({
step,
workspaceId,
});
sourceToClonedPairs.push({
source: step,
duplicated: clonedStep,
});
oldToNewIdMap.set(step.id, clonedStep.id);
}
const remappedTrigger = isDefined(newTrigger)
? {
...newTrigger,
nextStepIds: (newTrigger.nextStepIds ?? []).map(
(oldId) => oldToNewIdMap.get(oldId) ?? oldId,
),
},
};
}
}
: undefined;
return remappedStep;
const remappedSteps: WorkflowAction[] = sourceToClonedPairs.map(
({ source, duplicated }) => {
const remappedStep = {
...duplicated,
nextStepIds: (source.nextStepIds ?? []).map(
(oldId) => oldToNewIdMap.get(oldId) ?? oldId,
),
};
if (
source.type === WorkflowActionType.ITERATOR &&
isDefined(source.settings?.input?.initialLoopStepIds)
) {
remappedStep.settings = {
...remappedStep.settings,
input: {
...remappedStep.settings.input,
initialLoopStepIds:
source.settings.input.initialLoopStepIds.map(
(oldId) => oldToNewIdMap.get(oldId) ?? oldId,
),
},
};
}
return remappedStep;
},
);
await workflowVersionRepository.update(newDraftVersion.id, {
steps: remappedSteps,
trigger: remappedTrigger,
});
return {
...newDraftVersion,
name: newDraftVersion.name ?? '',
steps: remappedSteps,
trigger: remappedTrigger ?? null,
};
},
);
await workflowVersionRepository.update(newDraftVersion.id, {
steps: remappedSteps,
trigger: remappedTrigger,
});
return {
...newDraftVersion,
name: newDraftVersion.name ?? '',
steps: remappedSteps,
trigger: remappedTrigger ?? null,
};
}
async updateWorkflowVersionPositions({
@@ -305,51 +323,63 @@ export class WorkflowVersionWorkspaceService {
positions: WorkflowStepPositionUpdateInput[];
workspaceId: string;
}) {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersion = await workflowVersionRepository.findOneOrFail({
where: {
id: workflowVersionId,
},
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
assertWorkflowVersionIsDraft(workflowVersion);
const workflowVersion = await workflowVersionRepository.findOneOrFail({
where: {
id: workflowVersionId,
},
});
const triggerPosition = positions.find(
(position) => position.id === TRIGGER_STEP_ID,
);
assertWorkflowVersionIsDraft(workflowVersion);
const updatedTrigger =
isDefined(triggerPosition) && isDefined(workflowVersion.trigger)
? {
...workflowVersion.trigger,
position: triggerPosition.position,
const triggerPosition = positions.find(
(position) => position.id === TRIGGER_STEP_ID,
);
const updatedTrigger =
isDefined(triggerPosition) && isDefined(workflowVersion.trigger)
? {
...workflowVersion.trigger,
position: triggerPosition.position,
}
: undefined;
const updatedSteps = workflowVersion.steps?.map((step) => {
const updatedStep = positions.find(
(position) => position.id === step.id,
);
if (updatedStep) {
return {
...step,
position: updatedStep.position,
};
}
: undefined;
const updatedSteps = workflowVersion.steps?.map((step) => {
const updatedStep = positions.find((position) => position.id === step.id);
return step;
});
if (updatedStep) {
return {
...step,
position: updatedStep.position,
const updatePayload = {
...(!isDefined(updatedTrigger) ? {} : { trigger: updatedTrigger }),
...(!isDefined(updatedSteps) ? {} : { steps: updatedSteps }),
};
}
return step;
});
const updatePayload = {
...(!isDefined(updatedTrigger) ? {} : { trigger: updatedTrigger }),
...(!isDefined(updatedSteps) ? {} : { steps: updatedSteps }),
};
await workflowVersionRepository.update(workflowVersionId, updatePayload);
await workflowVersionRepository.update(
workflowVersionId,
updatePayload,
);
},
);
}
}
@@ -7,6 +7,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
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 { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { RESUME_DELAYED_WORKFLOW_JOB_NAME } from 'src/modules/workflow/workflow-executor/workflow-actions/delay/contants/resume-delayed-workflow-job-name';
import { isWorkflowDelayAction } from 'src/modules/workflow/workflow-executor/workflow-actions/delay/guards/is-workflow-delay-action.guard';
@@ -28,6 +30,7 @@ export class ResumeDelayedWorkflowJob {
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
@Process(RESUME_DELAYED_WORKFLOW_JOB_NAME)
@@ -36,67 +39,74 @@ export class ResumeDelayedWorkflowJob {
workflowRunId,
stepId,
}: ResumeDelayedWorkflowJobData): Promise<void> {
try {
const workflowRun =
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
workflowRunId,
workspaceId,
});
const authContext = buildSystemAuthContext(workspaceId);
if (workflowRun.status !== WorkflowRunStatus.RUNNING) {
return;
}
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
try {
const workflowRun =
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
workflowRunId,
workspaceId,
});
const step = workflowRun.state?.flow?.steps?.find(
(step) => step.id === stepId,
);
if (workflowRun.status !== WorkflowRunStatus.RUNNING) {
return;
}
const stepInfo = workflowRun.state?.stepInfos[stepId];
const step = workflowRun.state?.flow?.steps?.find(
(step) => step.id === stepId,
);
if (!step || !isWorkflowDelayAction(step)) {
throw new WorkflowRunException(
'Step not found or is not a delay action',
WorkflowRunExceptionCode.INVALID_INPUT,
);
}
const stepInfo = workflowRun.state?.stepInfos[stepId];
if (stepInfo?.status !== StepStatus.PENDING) {
throw new WorkflowRunException(
'Step is not pending',
WorkflowRunExceptionCode.INVALID_INPUT,
);
}
if (!step || !isWorkflowDelayAction(step)) {
throw new WorkflowRunException(
'Step not found or is not a delay action',
WorkflowRunExceptionCode.INVALID_INPUT,
);
}
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
stepId,
stepInfo: {
status: StepStatus.SUCCESS,
result: {
success: true,
},
},
workspaceId,
workflowRunId,
});
if (stepInfo?.status !== StepStatus.PENDING) {
throw new WorkflowRunException(
'Step is not pending',
WorkflowRunExceptionCode.INVALID_INPUT,
);
}
await this.messageQueueService.add<RunWorkflowJobData>(
RunWorkflowJob.name,
{
workspaceId,
workflowRunId,
lastExecutedStepId: stepId,
},
);
} catch (error) {
await this.workflowRunWorkspaceService.endWorkflowRun({
workflowRunId,
workspaceId,
status: WorkflowRunStatus.FAILED,
error:
error instanceof Error
? error.message
: 'Unknown error during delay resume',
});
}
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
stepId,
stepInfo: {
status: StepStatus.SUCCESS,
result: {
success: true,
},
},
workspaceId,
workflowRunId,
});
await this.messageQueueService.add<RunWorkflowJobData>(
RunWorkflowJob.name,
{
workspaceId,
workflowRunId,
lastExecutedStepId: stepId,
},
);
} catch (error) {
await this.workflowRunWorkspaceService.endWorkflowRun({
workflowRunId,
workspaceId,
status: WorkflowRunStatus.FAILED,
error:
error instanceof Error
? error.message
: 'Unknown error during delay resume',
});
}
},
);
}
}
@@ -7,6 +7,8 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
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 { 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 { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
@@ -26,6 +28,7 @@ export class RunWorkflowJob {
private readonly workflowExecutorWorkspaceService: WorkflowExecutorWorkspaceService,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly metricsService: MetricsService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
@Process(RUN_WORKFLOW_JOB_NAME)
@@ -34,27 +37,34 @@ export class RunWorkflowJob {
lastExecutedStepId,
workspaceId,
}: RunWorkflowJobData): Promise<void> {
try {
if (lastExecutedStepId) {
await this.resumeWorkflowExecution({
workspaceId,
workflowRunId,
lastExecutedStepId,
});
} else {
await this.startWorkflowExecution({
workflowRunId,
workspaceId,
});
}
} catch (error) {
await this.workflowRunWorkspaceService.endWorkflowRun({
workspaceId,
workflowRunId,
status: WorkflowRunStatus.FAILED,
error: error.message,
});
}
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
try {
if (lastExecutedStepId) {
await this.resumeWorkflowExecution({
workspaceId,
workflowRunId,
lastExecutedStepId,
});
} else {
await this.startWorkflowExecution({
workflowRunId,
workspaceId,
});
}
} catch (error) {
await this.workflowRunWorkspaceService.endWorkflowRun({
workspaceId,
workflowRunId,
status: WorkflowRunStatus.FAILED,
error: error.message,
});
}
},
);
}
private async startWorkflowExecution({
@@ -9,7 +9,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
WorkflowRunStatus,
@@ -27,7 +28,7 @@ export class WorkflowCleanWorkflowRunsJob {
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
@@ -47,37 +48,44 @@ export class WorkflowCleanWorkflowRunsJob {
for (const activeWorkspace of activeWorkspaces) {
const schemaName = getWorkspaceSchemaName(activeWorkspace.id);
const workflowRunsToDelete = await this.coreDataSource.query(
`
WITH ranked_runs AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY "workflowId"
ORDER BY "createdAt" DESC
) AS rn,
"createdAt"
FROM ${schemaName}."workflowRun"
WHERE status IN ('${WorkflowRunStatus.COMPLETED}', '${WorkflowRunStatus.FAILED}')
)
SELECT id, rn FROM ranked_runs
WHERE rn > ${NUMBER_OF_WORKFLOW_RUNS_TO_KEEP}
OR "createdAt" < NOW() - INTERVAL '14 days';
`,
);
const authContext = buildSystemAuthContext(activeWorkspace.id);
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
activeWorkspace.id,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunsToDelete = await this.coreDataSource.query(
`
WITH ranked_runs AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY "workflowId"
ORDER BY "createdAt" DESC
) AS rn,
"createdAt"
FROM ${schemaName}."workflowRun"
WHERE status IN ('${WorkflowRunStatus.COMPLETED}', '${WorkflowRunStatus.FAILED}')
)
SELECT id, rn FROM ranked_runs
WHERE rn > ${NUMBER_OF_WORKFLOW_RUNS_TO_KEEP}
OR "createdAt" < NOW() - INTERVAL '14 days';
`,
);
for (const workflowRunToDelete of workflowRunsToDelete) {
await workflowRunRepository.delete(workflowRunToDelete.id);
}
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository(
activeWorkspace.id,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
this.logger.log(
`Deleted ${workflowRunsToDelete.length} workflow runs for workspace ${activeWorkspace.id} (schema ${schemaName})`,
for (const workflowRunToDelete of workflowRunsToDelete) {
await workflowRunRepository.delete(workflowRunToDelete.id);
}
this.logger.log(
`Deleted ${workflowRunsToDelete.length} workflow runs for workspace ${activeWorkspace.id} (schema ${schemaName})`,
);
},
);
}
}
@@ -2,7 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { IsNull, LessThan, Or } from 'typeorm';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowRunStatus,
WorkflowRunWorkspaceEntity,
@@ -15,43 +16,50 @@ export class WorkflowHandleStaledRunsWorkspaceService {
WorkflowHandleStaledRunsWorkspaceService.name,
);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowThrottlingWorkspaceService: WorkflowThrottlingWorkspaceService,
) {}
async handleStaledRuns({ workspaceIds }: { workspaceIds: string[] }) {
for (const workspaceId of workspaceIds) {
try {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const staledWorkflowRuns = await workflowRunRepository.find({
where: {
status: WorkflowRunStatus.ENQUEUED,
enqueuedAt: Or(LessThan(oneHourAgo), IsNull()),
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const staledWorkflowRuns = await workflowRunRepository.find({
where: {
status: WorkflowRunStatus.ENQUEUED,
enqueuedAt: Or(LessThan(oneHourAgo), IsNull()),
},
});
if (staledWorkflowRuns.length <= 0) {
return;
}
await workflowRunRepository.update(
staledWorkflowRuns.map((workflowRun) => workflowRun.id),
{
enqueuedAt: null,
status: WorkflowRunStatus.NOT_STARTED,
},
);
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
},
});
if (staledWorkflowRuns.length <= 0) {
continue;
}
await workflowRunRepository.update(
staledWorkflowRuns.map((workflowRun) => workflowRun.id),
{
enqueuedAt: null,
status: WorkflowRunStatus.NOT_STARTED,
},
);
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
} catch (error) {
this.logger.error(
@@ -7,7 +7,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowRunStatus,
WorkflowRunWorkspaceEntity,
@@ -21,7 +22,7 @@ export class WorkflowRunEnqueueWorkspaceService {
private readonly logger = new Logger(WorkflowRunEnqueueWorkspaceService.name);
constructor(
private readonly workflowThrottlingWorkspaceService: WorkflowThrottlingWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
private readonly metricsService: MetricsService,
@@ -56,100 +57,107 @@ export class WorkflowRunEnqueueWorkspaceService {
}
try {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const notStartedRunsCount = isCacheMode
? await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromCache(
workspaceId,
)
: await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromDatabase(
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const notStartedRunsCount = isCacheMode
? await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromCache(
workspaceId,
)
: await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromDatabase(
workspaceId,
);
if (notStartedRunsCount <= 0) {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
return;
}
let remainingWorkflowRunToEnqueueCount =
await this.workflowThrottlingWorkspaceService.getRemainingRunsToEnqueueCount(
workspaceId,
);
const workflowRunIdsToEnqueue: string[] = [];
if (remainingWorkflowRunToEnqueueCount > 0) {
const additionalRunsToEnqueue = await workflowRunRepository.find({
where: {
status: WorkflowRunStatus.NOT_STARTED,
...(workflowRunIdsToEnqueue.length > 0
? { id: Not(workflowRunIdsToEnqueue[0]) }
: {}),
},
select: {
id: true,
},
order: {
createdAt: 'ASC',
},
take: remainingWorkflowRunToEnqueueCount,
});
workflowRunIdsToEnqueue.push(
...additionalRunsToEnqueue.map(
(workflowRun: WorkflowRunWorkspaceEntity) => workflowRun.id,
),
);
}
if (workflowRunIdsToEnqueue.length <= 0) {
if (!isCacheMode) {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
}
return;
}
await workflowRunRepository.update(workflowRunIdsToEnqueue, {
enqueuedAt: new Date().toISOString(),
status: WorkflowRunStatus.ENQUEUED,
});
await this.workflowThrottlingWorkspaceService.consumeRemainingRunsToEnqueueCount(
workspaceId,
workflowRunIdsToEnqueue.length,
);
if (notStartedRunsCount <= 0) {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
for (const workflowRunId of workflowRunIdsToEnqueue) {
await this.messageQueueService.add<RunWorkflowJobData>(
RunWorkflowJob.name,
{
workflowRunId,
workspaceId,
},
);
}
return;
}
let remainingWorkflowRunToEnqueueCount =
await this.workflowThrottlingWorkspaceService.getRemainingRunsToEnqueueCount(
workspaceId,
);
const workflowRunIdsToEnqueue: string[] = [];
if (remainingWorkflowRunToEnqueueCount > 0) {
const additionalRunsToEnqueue = await workflowRunRepository.find({
where: {
status: WorkflowRunStatus.NOT_STARTED,
...(workflowRunIdsToEnqueue.length > 0
? { id: Not(workflowRunIdsToEnqueue[0]) }
: {}),
},
select: {
id: true,
},
order: {
createdAt: 'ASC',
},
take: remainingWorkflowRunToEnqueueCount,
});
workflowRunIdsToEnqueue.push(
...additionalRunsToEnqueue.map(
(workflowRun: WorkflowRunWorkspaceEntity) => workflowRun.id,
),
);
}
if (workflowRunIdsToEnqueue.length <= 0) {
if (!isCacheMode) {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
}
return;
}
await workflowRunRepository.update(workflowRunIdsToEnqueue, {
enqueuedAt: new Date().toISOString(),
status: WorkflowRunStatus.ENQUEUED,
});
await this.workflowThrottlingWorkspaceService.consumeRemainingRunsToEnqueueCount(
workspaceId,
workflowRunIdsToEnqueue.length,
if (isCacheMode) {
await this.workflowThrottlingWorkspaceService.decreaseWorkflowRunNotStartedCount(
workspaceId,
workflowRunIdsToEnqueue.length,
);
} else {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
}
},
);
for (const workflowRunId of workflowRunIdsToEnqueue) {
await this.messageQueueService.add<RunWorkflowJobData>(
RunWorkflowJob.name,
{
workflowRunId,
workspaceId,
},
);
}
if (isCacheMode) {
await this.workflowThrottlingWorkspaceService.decreaseWorkflowRunNotStartedCount(
workspaceId,
workflowRunIdsToEnqueue.length,
);
} else {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
}
} catch (error) {
this.metricsService.incrementCounter({
key: MetricsKeys.WorkflowRunFailedToEnqueue,
@@ -7,7 +7,8 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowRunStatus,
WorkflowRunWorkspaceEntity,
@@ -18,7 +19,7 @@ export class WorkflowThrottlingWorkspaceService {
constructor(
@InjectCacheStorage(CacheStorageNamespace.ModuleWorkflow)
private readonly cacheStorage: CacheStorageService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly throttlerService: ThrottlerService,
private readonly twentyConfigService: TwentyConfigService,
) {}
@@ -75,19 +76,26 @@ export class WorkflowThrottlingWorkspaceService {
async recomputeWorkflowRunNotStartedCount(
workspaceId: string,
): Promise<void> {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const currentlyNotStartedWorkflowRunCount =
await workflowRunRepository.count({
where: {
status: In([WorkflowRunStatus.NOT_STARTED]),
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
return workflowRunRepository.count({
where: {
status: In([WorkflowRunStatus.NOT_STARTED]),
},
});
},
});
);
await this.setWorkflowRunNotStartedCount(
workspaceId,
@@ -102,18 +110,25 @@ export class WorkflowThrottlingWorkspaceService {
async getNotStartedRunsCountFromDatabase(
workspaceId: string,
): Promise<number> {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
return workflowRunRepository.count({
where: {
status: In([WorkflowRunStatus.NOT_STARTED]),
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
WorkflowRunWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
return workflowRunRepository.count({
where: {
status: In([WorkflowRunStatus.NOT_STARTED]),
},
});
},
});
);
}
async acquireWorkflowEnqueueLock(
@@ -7,7 +7,8 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 { type WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
@Command({
@@ -20,10 +21,10 @@ export class DeleteWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrat
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
@Option({
@@ -50,31 +51,40 @@ export class DeleteWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrat
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
try {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const createdAtCondition = {
createdAt: LessThan(this.createdBeforeDate || new Date().toISOString()),
};
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
try {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const workflowRunCount = await workflowRunRepository.count({
where: createdAtCondition,
});
const createdAtCondition = {
createdAt: LessThan(
this.createdBeforeDate || new Date().toISOString(),
),
};
if (!options.dryRun && workflowRunCount > 0) {
await workflowRunRepository.delete(createdAtCondition);
}
const workflowRunCount = await workflowRunRepository.count({
where: createdAtCondition,
});
this.logger.log(
`${options.dryRun ? ' (DRY RUN): ' : ''}Deleted ${workflowRunCount} workflow runs`,
);
} catch (error) {
this.logger.error('Error while deleting workflowRun', error);
}
if (!options.dryRun && workflowRunCount > 0) {
await workflowRunRepository.delete(createdAtCondition);
}
this.logger.log(
`${options.dryRun ? ' (DRY RUN): ' : ''}Deleted ${workflowRunCount} workflow runs`,
);
} catch (error) {
this.logger.error('Error while deleting workflowRun', error);
}
},
);
}
}
@@ -10,7 +10,8 @@ import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowRunStatus,
type WorkflowRunState,
@@ -27,7 +28,7 @@ import {
@Injectable()
export class WorkflowRunWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly recordPositionService: RecordPositionService,
private readonly metricsService: MetricsService,
@@ -53,78 +54,89 @@ export class WorkflowRunWorkspaceService {
error?: string;
workspaceId: string;
}) {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workspaceId,
workflowVersionId,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workspaceId,
workflowVersionId,
});
const workflow = await workflowRepository.findOne({
where: {
id: workflowVersion.workflowId,
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const workflow = await workflowRepository.findOne({
where: {
id: workflowVersion.workflowId,
},
});
if (!workflow) {
throw new WorkflowRunException(
'Workflow id is invalid',
WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID,
);
}
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowRun',
},
workspaceId,
});
const initState = this.getInitState(
workflowVersion,
triggerPayload,
error,
);
const lastWorkflowRun = await workflowRunRepository.findOne({
where: {
workflowId: workflow.id,
},
order: { createdAt: 'desc' },
});
const workflowRunCountMatch = lastWorkflowRun?.name?.match(/#(\d+)/);
const workflowRunCount = workflowRunCountMatch
? parseInt(workflowRunCountMatch[1], 10)
: 0;
const workflowRun = {
id: workflowRunId ?? v4(),
name: `#${workflowRunCount + 1} - ${workflow.name}`,
workflowVersionId,
createdBy,
workflowId: workflow.id,
status,
position,
state: initState,
enqueuedAt: status === WorkflowRunStatus.ENQUEUED ? new Date() : null,
};
await workflowRunRepository.insert(workflowRun);
return workflowRun.id;
},
});
if (!workflow) {
throw new WorkflowRunException(
'Workflow id is invalid',
WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID,
);
}
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowRun',
},
workspaceId,
});
const initState = this.getInitState(workflowVersion, triggerPayload, error);
const lastWorkflowRun = await workflowRunRepository.findOne({
where: {
workflowId: workflow.id,
},
order: { createdAt: 'desc' },
});
const workflowRunCountMatch = lastWorkflowRun?.name?.match(/#(\d+)/);
const workflowRunCount = workflowRunCountMatch
? parseInt(workflowRunCountMatch[1], 10)
: 0;
const workflowRun = {
id: workflowRunId ?? v4(),
name: `#${workflowRunCount + 1} - ${workflow.name}`,
workflowVersionId,
createdBy,
workflowId: workflow.id,
status,
position,
state: initState,
enqueuedAt: status === WorkflowRunStatus.ENQUEUED ? new Date() : null,
};
await workflowRunRepository.insert(workflowRun);
return workflowRun.id;
);
}
@WithLock('workflowRunId')
@@ -328,16 +340,23 @@ export class WorkflowRunWorkspaceService {
workflowRunId: string;
workspaceId: string;
}): Promise<WorkflowRunWorkspaceEntity | null> {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
return await workflowRunRepository.findOne({
where: { id: workflowRunId },
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
return await workflowRunRepository.findOne({
where: { id: workflowRunId },
});
},
);
}
async getWorkflowRunOrFail({
@@ -371,29 +390,36 @@ export class WorkflowRunWorkspaceService {
workspaceId: string;
partialUpdate: QueryDeepPartialEntity<WorkflowRunWorkspaceEntity>;
}) {
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
id: workflowRunId,
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
if (!workflowRunToUpdate) {
throw new WorkflowRunException(
`workflowRun ${workflowRunId} not found`,
WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND,
);
}
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
id: workflowRunId,
});
await workflowRunRepository.update(
workflowRunToUpdate.id,
partialUpdate,
undefined,
['id'],
if (!workflowRunToUpdate) {
throw new WorkflowRunException(
`workflowRun ${workflowRunId} not found`,
WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND,
);
}
await workflowRunRepository.update(
workflowRunToUpdate.id,
partialUpdate,
undefined,
['id'],
);
},
);
}
@@ -4,7 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import {
@@ -27,8 +27,8 @@ describe('WorkflowStatusesUpdate', () => {
update: jest.fn(),
};
const mockTwentyORMGlobalManager = {
getRepositoryForWorkspace: jest
const mockGlobalWorkspaceOrmManager = {
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity, options) => {
if (!options?.shouldBypassPermissionChecks) {
@@ -46,6 +46,10 @@ describe('WorkflowStatusesUpdate', () => {
return Promise.resolve(null);
}),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
};
const mockServerlessFunctionService = {
@@ -58,8 +62,8 @@ describe('WorkflowStatusesUpdate', () => {
providers: [
WorkflowStatusesUpdateJob,
{
provide: TwentyORMGlobalManager,
useValue: mockTwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: mockGlobalWorkspaceOrmManager,
},
{
provide: ServerlessFunctionService,
@@ -8,8 +8,9 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-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';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import {
WorkflowVersionStepException,
WorkflowVersionStepExceptionCode,
@@ -69,37 +70,44 @@ export class WorkflowStatusesUpdateJob {
protected readonly logger = new Logger(WorkflowStatusesUpdateJob.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly serverlessFunctionService: ServerlessFunctionService,
) {}
@Process(WorkflowStatusesUpdateJob.name)
async handle(event: WorkflowVersionBatchEvent): Promise<void> {
switch (event.type) {
case WorkflowVersionEventType.CREATE:
case WorkflowVersionEventType.DELETE:
await Promise.all(
event.workflowIds.map((workflowId) =>
this.handleWorkflowVersionCreatedOrDeleted({
workflowId,
workspaceId: event.workspaceId,
}),
),
);
break;
case WorkflowVersionEventType.STATUS_UPDATE:
await Promise.all(
event.statusUpdates.map((statusUpdate) =>
this.handleWorkflowVersionStatusUpdated({
statusUpdate,
workspaceId: event.workspaceId,
}),
),
);
break;
default:
break;
}
const authContext = buildSystemAuthContext(event.workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
switch (event.type) {
case WorkflowVersionEventType.CREATE:
case WorkflowVersionEventType.DELETE:
await Promise.all(
event.workflowIds.map((workflowId) =>
this.handleWorkflowVersionCreatedOrDeleted({
workflowId,
workspaceId: event.workspaceId,
}),
),
);
break;
case WorkflowVersionEventType.STATUS_UPDATE:
await Promise.all(
event.statusUpdates.map((statusUpdate) =>
this.handleWorkflowVersionStatusUpdated({
statusUpdate,
workspaceId: event.workspaceId,
}),
),
);
break;
default:
break;
}
},
);
}
private async handleWorkflowVersionCreatedOrDeleted({
@@ -110,14 +118,14 @@ export class WorkflowStatusesUpdateJob {
workspaceId: string;
}): Promise<void> {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
@@ -212,14 +220,14 @@ export class WorkflowStatusesUpdateJob {
workspaceId: string;
}): Promise<void> {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
@@ -4,7 +4,7 @@ import { type ToolSet } from 'ai';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { PerObjectToolGeneratorService } from 'src/engine/core-modules/tool-generator/services/per-object-tool-generator.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
@@ -39,7 +39,7 @@ export class WorkflowToolWorkspaceService {
workflowVersionService: WorkflowVersionWorkspaceService,
workflowTriggerService: WorkflowTriggerWorkspaceService,
workflowSchemaService: WorkflowSchemaWorkspaceService,
twentyORMGlobalManager: TwentyORMGlobalManager,
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
recordPositionService: RecordPositionService,
private readonly perObjectToolGenerator: PerObjectToolGeneratorService,
) {
@@ -49,7 +49,7 @@ export class WorkflowToolWorkspaceService {
workflowVersionService,
workflowTriggerService,
workflowSchemaService,
twentyORMGlobalManager,
globalWorkspaceOrmManager,
recordPositionService,
};
@@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@@ -63,7 +64,7 @@ type CreateCompleteWorkflowToolDeps = Pick<
| 'workflowVersionService'
| 'workflowVersionEdgeService'
| 'workflowTriggerService'
| 'twentyORMGlobalManager'
| 'globalWorkspaceOrmManager'
| 'recordPositionService'
>;
@@ -196,34 +197,40 @@ const createWorkflow = async ({
context: CreateCompleteWorkflowToolContext;
name: string;
}): Promise<string> => {
const workflowRepository =
await deps.twentyORMGlobalManager.getRepositoryForWorkspace(
context.workspaceId,
'workflow',
context.rolePermissionConfig,
);
const authContext = buildSystemAuthContext(context.workspaceId);
const workflowPosition = await deps.recordPositionService.buildRecordPosition(
{
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId: context.workspaceId,
return deps.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRepository =
await deps.globalWorkspaceOrmManager.getRepository(
context.workspaceId,
'workflow',
context.rolePermissionConfig,
);
const workflowPosition =
await deps.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId: context.workspaceId,
});
const workflow = {
id: uuidv4(),
name,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
};
await workflowRepository.insert(workflow);
return workflow.id;
},
);
const workflow = {
id: uuidv4(),
name,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
};
await workflowRepository.insert(workflow);
return workflow.id;
};
const createWorkflowVersion = async ({
@@ -239,35 +246,43 @@ const createWorkflowVersion = async ({
trigger: WorkflowTrigger;
steps: WorkflowAction[];
}): Promise<string> => {
const workflowVersionRepository =
await deps.twentyORMGlobalManager.getRepositoryForWorkspace(
context.workspaceId,
'workflowVersion',
context.rolePermissionConfig,
);
const authContext = buildSystemAuthContext(context.workspaceId);
const versionPosition = await deps.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
return deps.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await deps.globalWorkspaceOrmManager.getRepository(
context.workspaceId,
'workflowVersion',
context.rolePermissionConfig,
);
const versionPosition =
await deps.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId: context.workspaceId,
});
const workflowVersion = {
id: uuidv4(),
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps,
position: versionPosition,
};
await workflowVersionRepository.insert(workflowVersion);
return workflowVersion.id;
},
workspaceId: context.workspaceId,
});
const workflowVersion = {
id: uuidv4(),
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps,
position: versionPosition,
};
await workflowVersionRepository.insert(workflowVersion);
return workflowVersion.id;
);
};
const updateWorkflowStatus = async ({
@@ -281,15 +296,22 @@ const updateWorkflowStatus = async ({
workflowId: string;
workflowVersionId: string;
}) => {
const workflowRepository =
await deps.twentyORMGlobalManager.getRepositoryForWorkspace(
context.workspaceId,
'workflow',
context.rolePermissionConfig,
);
const authContext = buildSystemAuthContext(context.workspaceId);
await workflowRepository.update(workflowId, {
statuses: [WorkflowStatus.ACTIVE],
lastPublishedVersionId: workflowVersionId,
});
await deps.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRepository =
await deps.globalWorkspaceOrmManager.getRepository(
context.workspaceId,
'workflow',
context.rolePermissionConfig,
);
await workflowRepository.update(workflowId, {
statuses: [WorkflowStatus.ACTIVE],
lastPublishedVersionId: workflowVersionId,
});
},
);
};
@@ -1,5 +1,5 @@
import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import type { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import type { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import type { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import type { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
import type { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
@@ -12,7 +12,7 @@ export type WorkflowToolDependencies = {
workflowVersionService: WorkflowVersionWorkspaceService;
workflowTriggerService: WorkflowTriggerWorkspaceService;
workflowSchemaService: WorkflowSchemaWorkspaceService;
twentyORMGlobalManager: TwentyORMGlobalManager;
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
recordPositionService: RecordPositionService;
};
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
type AutomatedTriggerType,
type WorkflowAutomatedTriggerWorkspaceEntity,
@@ -10,7 +11,7 @@ import { type AutomatedTriggerSettings } from 'src/modules/workflow/workflow-tri
@Injectable()
export class AutomatedTriggerWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async addAutomatedTrigger({
@@ -24,17 +25,24 @@ export class AutomatedTriggerWorkspaceService {
settings: AutomatedTriggerSettings;
workspaceId: string;
}) {
const workflowAutomatedTriggerRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
'workflowAutomatedTrigger',
);
const authContext = buildSystemAuthContext(workspaceId);
await workflowAutomatedTriggerRepository.insert({
type,
settings,
workflowId,
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowAutomatedTriggerRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
'workflowAutomatedTrigger',
);
await workflowAutomatedTriggerRepository.insert({
type,
settings,
workflowId,
});
},
);
}
async deleteAutomatedTrigger({
@@ -44,12 +52,19 @@ export class AutomatedTriggerWorkspaceService {
workflowId: string;
workspaceId: string;
}) {
const workflowAutomatedTriggerRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
'workflowAutomatedTrigger',
);
const authContext = buildSystemAuthContext(workspaceId);
await workflowAutomatedTriggerRepository.delete({ workflowId });
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowAutomatedTriggerRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
'workflowAutomatedTrigger',
);
await workflowAutomatedTriggerRepository.delete({ workflowId });
},
);
}
}
@@ -2,7 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@@ -11,7 +11,7 @@ import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/w
describe('WorkflowDatabaseEventTriggerListener', () => {
let listener: WorkflowDatabaseEventTriggerListener;
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let messageQueueService: jest.Mocked<MessageQueueService>;
const mockRepository = {
@@ -48,8 +48,11 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
}) as FlatObjectMetadata;
beforeEach(async () => {
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository),
globalWorkspaceOrmManager = {
getRepository: jest.fn().mockResolvedValue(mockRepository),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
} as any;
messageQueueService = {
@@ -60,8 +63,8 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
providers: [
WorkflowDatabaseEventTriggerListener,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
{
provide: MessageQueueService,
@@ -20,7 +20,8 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import {
AutomatedTriggerType,
@@ -43,7 +44,7 @@ export class WorkflowDatabaseEventTriggerListener {
);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
@@ -242,57 +243,66 @@ export class WorkflowDatabaseEventTriggerListener {
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}) {
const { fieldIdByJoinColumnName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const { fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
for (const [joinColumnName, joinFieldId] of Object.entries(
fieldIdByJoinColumnName,
)) {
const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityId: joinFieldId,
});
const joinRecordIds = records
.map((record) => record[joinColumnName])
.filter(isDefined);
if (joinRecordIds.length === 0) {
continue;
}
const relatedObjectMetadataId =
joinField.relationTargetObjectMetadataId;
if (!isDefined(relatedObjectMetadataId)) {
continue;
}
const relatedObjectMetadataNameSingular =
flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
if (!isDefined(relatedObjectMetadataNameSingular)) {
continue;
}
const relatedObjectRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
relatedObjectMetadataNameSingular,
{ shouldBypassPermissionChecks: true },
);
const relatedRecords = await relatedObjectRepository.find({
where: { id: In(joinRecordIds) },
});
for (const record of records) {
record[joinField.name] = relatedRecords.find(
(relatedRecord) => relatedRecord.id === record[joinColumnName],
);
}
}
},
);
for (const [joinColumnName, joinFieldId] of Object.entries(
fieldIdByJoinColumnName,
)) {
const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityId: joinFieldId,
});
const joinRecordIds = records
.map((record) => record[joinColumnName])
.filter(isDefined);
if (joinRecordIds.length === 0) {
continue;
}
const relatedObjectMetadataId = joinField.relationTargetObjectMetadataId;
if (!isDefined(relatedObjectMetadataId)) {
continue;
}
const relatedObjectMetadataNameSingular =
flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
if (!isDefined(relatedObjectMetadataNameSingular)) {
continue;
}
const relatedObjectRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
relatedObjectMetadataNameSingular,
{ shouldBypassPermissionChecks: true },
);
const relatedRecords = await relatedObjectRepository.find({
where: { id: In(joinRecordIds) },
});
for (const record of records) {
record[joinField.name] = relatedRecords.find(
(relatedRecord) => relatedRecord.id === record[joinColumnName],
);
}
}
}
private async shouldIgnoreEvent(
@@ -325,45 +335,52 @@ export class WorkflowDatabaseEventTriggerListener {
const databaseEventName = payload.name;
const automatedTriggerTableName = 'workflowAutomatedTrigger';
const workflowAutomatedTriggerRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
automatedTriggerTableName,
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const eventListeners = await workflowAutomatedTriggerRepository.find({
where: {
type: AutomatedTriggerType.DATABASE_EVENT,
settings: Raw(
() =>
`"${automatedTriggerTableName}"."settings"->>'eventName' = :eventName`,
{ eventName: databaseEventName },
),
},
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowAutomatedTriggerRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
workspaceId,
automatedTriggerTableName,
{ shouldBypassPermissionChecks: true },
);
for (const eventListener of eventListeners) {
for (const eventPayload of payload.events) {
const shouldTriggerJob = this.shouldTriggerJob({
eventPayload,
eventListener,
action,
const eventListeners = await workflowAutomatedTriggerRepository.find({
where: {
type: AutomatedTriggerType.DATABASE_EVENT,
settings: Raw(
() =>
`"${automatedTriggerTableName}"."settings"->>'eventName' = :eventName`,
{ eventName: databaseEventName },
),
},
});
if (shouldTriggerJob) {
await this.messageQueueService.add<WorkflowTriggerJobData>(
WorkflowTriggerJob.name,
{
workspaceId,
workflowId: eventListener.workflowId,
payload: eventPayload,
},
{ retryLimit: 3 },
);
for (const eventListener of eventListeners) {
for (const eventPayload of payload.events) {
const shouldTriggerJob = this.shouldTriggerJob({
eventPayload,
eventListener,
action,
});
if (shouldTriggerJob) {
await this.messageQueueService.add<WorkflowTriggerJobData>(
WorkflowTriggerJob.name,
{
workspaceId,
workflowId: eventListener.workflowId,
payload: eventPayload,
},
{ retryLimit: 3 },
);
}
}
}
}
}
},
);
}
private shouldTriggerJob({
@@ -10,7 +10,8 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { handleWorkflowTriggerException } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 {
WorkflowVersionStatus,
type WorkflowVersionWorkspaceEntity,
@@ -33,7 +34,7 @@ const DEFAULT_WORKFLOW_NAME = 'Workflow';
@Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST })
export class WorkflowTriggerJob {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
@@ -41,77 +42,83 @@ export class WorkflowTriggerJob {
@Process(WorkflowTriggerJob.name)
async handle(data: WorkflowTriggerJobData): Promise<void> {
try {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
data.workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(data.workspaceId);
const workflow = await workflowRepository.findOneBy({
id: data.workflowId,
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
try {
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
data.workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
if (!workflow) {
throw new WorkflowTriggerException(
`Workflow ${data.workflowId} not found in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
const workflow = await workflowRepository.findOneBy({
id: data.workflowId,
});
if (!workflow.lastPublishedVersionId) {
throw new WorkflowTriggerException(
`Workflow ${data.workflowId} has no published version in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
if (!workflow) {
throw new WorkflowTriggerException(
`Workflow ${data.workflowId} not found in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
data.workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
if (!workflow.lastPublishedVersionId) {
throw new WorkflowTriggerException(
`Workflow ${data.workflowId} has no published version in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
const workflowVersion = await workflowVersionRepository.findOneBy({
id: workflow.lastPublishedVersionId,
});
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
data.workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
if (!workflowVersion) {
throw new WorkflowTriggerException(
`Workflow version ${workflow.lastPublishedVersionId} not found in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
`Workflow version ${workflowVersion.id} is not active in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
const workflowVersion = await workflowVersionRepository.findOneBy({
id: workflow.lastPublishedVersionId,
});
await this.workflowRunnerWorkspaceService.run({
workspaceId: data.workspaceId,
workflowVersionId: workflow.lastPublishedVersionId,
payload: data.payload,
source: {
source: FieldActorSource.WORKFLOW,
name:
isDefined(workflow.name) && !isEmpty(workflow.name)
? workflow.name
: DEFAULT_WORKFLOW_NAME,
context: {},
workspaceMemberId: null,
},
});
} catch (e) {
// We remove cron if it exists when no valid workflowVersion exists
await this.messageQueueService.removeCron({
jobName: WorkflowTriggerJob.name,
jobId: data.workflowId,
});
handleWorkflowTriggerException(e);
}
if (!workflowVersion) {
throw new WorkflowTriggerException(
`Workflow version ${workflow.lastPublishedVersionId} not found in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
`Workflow version ${workflowVersion.id} is not active in workspace ${data.workspaceId}`,
WorkflowTriggerExceptionCode.INTERNAL_ERROR,
);
}
await this.workflowRunnerWorkspaceService.run({
workspaceId: data.workspaceId,
workflowVersionId: workflow.lastPublishedVersionId,
payload: data.payload,
source: {
source: FieldActorSource.WORKFLOW,
name:
isDefined(workflow.name) && !isEmpty(workflow.name)
? workflow.name
: DEFAULT_WORKFLOW_NAME,
context: {},
workspaceMemberId: null,
},
});
} catch (e) {
await this.messageQueueService.removeCron({
jobName: WorkflowTriggerJob.name,
jobId: data.workflowId,
});
handleWorkflowTriggerException(e);
}
},
);
}
}
@@ -3,8 +3,9 @@ import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type ActorMetadata } from 'twenty-shared/types';
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';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
import {
@@ -31,7 +32,7 @@ import { assertNever } from 'src/utils/assert';
@Injectable()
export class WorkflowTriggerWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
private readonly automatedTriggerWorkspaceService: AutomatedTriggerWorkspaceService,
@@ -69,71 +70,87 @@ export class WorkflowTriggerWorkspaceService {
workflowVersionId: string,
workspaceId: string,
) {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const authContext = buildSystemAuthContext(workspaceId);
const workflowVersionNullable = await workflowVersionRepository.findOne({
where: { id: workflowVersionId },
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion =
await this.workflowCommonWorkspaceService.getValidWorkflowVersionOrFail(
workflowVersionNullable,
);
const workflowVersionNullable = await workflowVersionRepository.findOne(
{
where: { id: workflowVersionId },
},
);
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
);
const workflowVersion =
await this.workflowCommonWorkspaceService.getValidWorkflowVersionOrFail(
workflowVersionNullable,
);
const workflow = await workflowRepository.findOne({
where: { id: workflowVersion.workflowId },
});
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
if (!workflow) {
throw new WorkflowTriggerException(
'No workflow found',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
const workflow = await workflowRepository.findOne({
where: { id: workflowVersion.workflowId },
});
assertVersionCanBeActivated(workflowVersion, workflow);
if (!workflow) {
throw new WorkflowTriggerException(
'No workflow found',
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
await this.performActivationSteps(
workflow,
workflowVersion,
workflowRepository,
workflowVersionRepository,
workspaceId,
assertVersionCanBeActivated(workflowVersion, workflow);
await this.performActivationSteps(
workflow,
workflowVersion,
workflowRepository,
workflowVersionRepository,
workspaceId,
);
return true;
},
);
return true;
}
async deactivateWorkflowVersion(
workflowVersionId: string,
workspaceId: string,
) {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
await this.performDeactivationSteps(
workflowVersionId,
workflowVersionRepository,
workspaceId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
await this.performDeactivationSteps(
workflowVersionId,
workflowVersionRepository,
workspaceId,
);
return true;
},
);
return true;
}
async stopWorkflowRun(workflowRunId: string, workspaceId: string) {