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