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
@@ -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,
);
},
);
}
}