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
View File
@@ -27,6 +27,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware';
import { MiddlewareModule } from 'src/engine/middlewares/middleware.module';
import { RestCoreMiddleware } from 'src/engine/middlewares/rest-core.middleware';
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { ModulesModule } from 'src/modules/modules.module';
@@ -57,6 +58,7 @@ const MIGRATED_REST_METHODS = [
useClass: GraphQLConfigService,
}),
TwentyORMModule,
GlobalWorkspaceDataSourceModule,
ClickHouseModule,
// Core engine module, contains all the core modules
CoreEngineModule,
@@ -12,7 +12,7 @@ import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/conf
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
class BasicUpgradeCommandRunner extends UpgradeCommandRunner {
@@ -84,14 +84,14 @@ const buildUpgradeCommandModule = async ({
useFactory: (
workspaceRepository: Repository<WorkspaceEntity>,
twentyConfigService: TwentyConfigService,
twentyORMGlobalManager: TwentyORMGlobalManager,
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
dataSourceService: DataSourceService,
syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
) => {
return new commandRunner(
workspaceRepository,
twentyConfigService,
twentyORMGlobalManager,
globalWorkspaceOrmManager,
dataSourceService,
syncWorkspaceMetadataCommand,
);
@@ -99,7 +99,7 @@ const buildUpgradeCommandModule = async ({
inject: [
getRepositoryToken(WorkspaceEntity),
TwentyConfigService,
TwentyORMGlobalManager,
GlobalWorkspaceOrmManager,
DataSourceService,
SyncWorkspaceMetadataCommand,
],
@@ -132,11 +132,14 @@ const buildUpgradeCommandModule = async ({
},
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
connect: jest.fn(),
destroyDataSourceForWorkspace: jest.fn(),
getDataSourceForWorkspace: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -162,7 +165,7 @@ describe('UpgradeCommandRunner', () => {
let runAfterSyncMetadataSpy: jest.SpyInstance;
let runBeforeSyncMetadataSpy: jest.SpyInstance;
let runCoreMigrationsSpy: jest.SpyInstance;
let twentyORMGlobalManagerSpy: TwentyORMGlobalManager;
let globalWorkspaceOrmManagerSpy: GlobalWorkspaceOrmManager;
type BuildModuleAndSetupSpiesArgs = {
numberOfWorkspace?: number;
@@ -215,8 +218,8 @@ describe('UpgradeCommandRunner', () => {
getRepositoryToken(WorkspaceEntity),
);
syncWorkspaceMetadataCommand = module.get(SyncWorkspaceMetadataCommand);
twentyORMGlobalManagerSpy = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManagerSpy = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
};
@@ -246,7 +249,7 @@ describe('UpgradeCommandRunner', () => {
expect(failReport.length).toBe(0);
[
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
upgradeCommandRunner.runOnWorkspace,
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
@@ -278,7 +281,7 @@ describe('UpgradeCommandRunner', () => {
upgradeCommandRunner.runBeforeSyncMetadata,
upgradeCommandRunner.runAfterSyncMetadata,
syncWorkspaceMetadataCommand.runOnWorkspace,
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace));
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
numberOfWorkspace,
@@ -303,7 +306,7 @@ describe('UpgradeCommandRunner', () => {
upgradeCommandRunner.runBeforeSyncMetadata,
upgradeCommandRunner.runAfterSyncMetadata,
syncWorkspaceMetadataCommand.runOnWorkspace,
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace,
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
// Verify order of execution
@@ -7,7 +7,7 @@ import {
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.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';
export type ActiveOrSuspendedWorkspacesMigrationCommandOptions =
WorkspacesMigrationCommandOptions;
@@ -18,10 +18,10 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
> extends WorkspacesMigrationCommandRunner<Options> {
constructor(
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, [
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]);
@@ -20,7 +20,7 @@ import {
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type 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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import {
type CompareVersionMajorAndMinorReturnType,
@@ -52,11 +52,11 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyConfigService: TwentyConfigService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
private async loadActiveOrSuspendedWorkspace() {
@@ -1,14 +1,16 @@
import chalk from 'chalk';
import { Option } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, MoreThanOrEqual, type Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import { MigrationCommandRunner } from 'src/database/commands/command-runners/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 { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
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 { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
export type WorkspacesMigrationCommandOptions = {
workspaceIds: string[];
@@ -21,7 +23,7 @@ export type WorkspacesMigrationCommandOptions = {
export type RunOnWorkspaceArgs = {
options: WorkspacesMigrationCommandOptions;
workspaceId: string;
dataSource?: WorkspaceDataSource;
dataSource?: WorkspaceDataSourceInterface;
index: number;
total: number;
};
@@ -50,7 +52,7 @@ export abstract class WorkspacesMigrationCommandRunner<
constructor(
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly activationStatuses: WorkspaceActivationStatus[],
) {
@@ -138,24 +140,32 @@ export abstract class WorkspacesMigrationCommandRunner<
);
try {
const workspaceHasDataSource =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
workspaceId,
);
const authContext = buildSystemAuthContext(workspaceId);
const dataSource = isDefined(workspaceHasDataSource)
? await this.twentyORMGlobalManager.getDataSourceForWorkspace({
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceHasDataSource =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
workspaceId,
);
const dataSource = isDefined(workspaceHasDataSource)
? await this.globalWorkspaceOrmManager.getDataSourceForWorkspace(
workspaceId,
)
: undefined;
await this.runOnWorkspace({
options,
workspaceId,
})
: undefined;
dataSource,
index: index,
total: workspaceIdsToProcess.length,
});
},
);
await this.runOnWorkspace({
options,
workspaceId,
dataSource,
index: index,
total: workspaceIdsToProcess.length,
});
this.migrationReport.success.push({
workspaceId,
});
@@ -170,7 +180,7 @@ export abstract class WorkspacesMigrationCommandRunner<
}
try {
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
await this.globalWorkspaceOrmManager.destroyDataSourceForWorkspace(
workspaceId,
);
} catch (error) {
@@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WORKFLOW_RUN_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
@@ -23,14 +23,14 @@ export class AddWorkflowRunStopStatusesCommand extends ActiveOrSuspendedWorkspac
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
@@ -11,7 +11,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
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';
@Command({
name: 'upgrade:1-10:clean-orphaned-kanban-aggregate-operation-field-metadata-id',
@@ -22,7 +22,7 @@ export class CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand extends
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@@ -7,7 +7,7 @@ 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';
@Command({
name: '1-10-create-view-kanban-field-metadata-id-foreign-key-migration',
@@ -20,7 +20,7 @@ export class CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand extends A
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -7,7 +7,7 @@ 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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Command({
@@ -18,7 +18,7 @@ export class FlushCacheCommand extends ActiveOrSuspendedWorkspacesMigrationComma
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {
@@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.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';
@Command({
name: 'upgrade:1-10:make-sure-dashboard-naming-available',
@@ -20,7 +20,7 @@ export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWo
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@@ -9,7 +9,7 @@ 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 { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -22,7 +22,7 @@ export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspended
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
@@ -36,13 +36,13 @@ export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspended
);
const attachmentRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepository<AttachmentWorkspaceEntity>(
workspaceId,
'attachment',
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
@@ -8,7 +8,7 @@ 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 { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
const TYPE_TO_FILE_CATEGORY_MAPPING: Record<string, string> = {
@@ -31,7 +31,7 @@ export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspende
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
@@ -45,7 +45,7 @@ export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspende
);
const attachmentRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepository<AttachmentWorkspaceEntity>(
workspaceId,
'attachment',
);
@@ -7,7 +7,7 @@ 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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@Command({
@@ -19,7 +19,7 @@ export class MigrateChannelPartialFullSyncStagesCommand extends ActiveOrSuspende
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -12,7 +12,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.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 { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@@ -40,7 +40,7 @@ export class RegenerateSearchVectorsCommand extends ActiveOrSuspendedWorkspacesM
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
@InjectRepository(ObjectMetadataEntity)
@@ -15,7 +15,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 { createCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views';
import { prefillWorkspaceFavorites } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workspace-favorites';
import { dashboardsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/dashboards-all.view';
@@ -30,7 +30,7 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati
private readonly coreDataSource: DataSource,
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@@ -9,7 +9,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.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';
@Command({
name: 'upgrade:1-11:clean-orphaned-role-targets',
@@ -23,7 +23,7 @@ export class CleanOrphanedRoleTargetsCommand extends ActiveOrSuspendedWorkspaces
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
@@ -8,7 +8,7 @@ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/worksp
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Command({
@@ -20,7 +20,7 @@ export class CleanOrphanedUserWorkspacesCommand extends ActiveOrSuspendedWorkspa
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
@@ -49,7 +49,7 @@ export class CleanOrphanedUserWorkspacesCommand extends ActiveOrSuspendedWorkspa
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
@@ -9,7 +9,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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 { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
@Command({
@@ -21,7 +21,7 @@ export class CreateTwentyStandardApplicationCommand extends ActiveOrSuspendedWor
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { CALENDAR_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
@@ -24,7 +24,7 @@ export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSu
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MESSAGE_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@@ -24,7 +24,7 @@ export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspende
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@@ -31,7 +31,7 @@ import {
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
@@ -46,7 +46,7 @@ export class CleanNullEquivalentValuesCommand extends ActiveOrSuspendedWorkspace
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
@InjectRepository(ObjectMetadataEntity)
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
@@ -12,7 +12,7 @@ import {
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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';
@Command({
name: 'upgrade:1-12:create-workspace-custom-application',
@@ -23,7 +23,7 @@ export class CreateWorkspaceCustomApplicationCommand extends WorkspacesMigration
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
) {
@@ -8,7 +8,7 @@ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/worksp
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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';
// Note: this command needs to be run after CreateWorkspaceCustomApplicationCommand
@Command({
@@ -19,7 +19,7 @@ export class SetStandardApplicationNotUninstallableCommand extends ActiveOrSuspe
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
) {
@@ -7,7 +7,7 @@ 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';
@Command({
name: '1-12-workspace-custom-application-id-non-nullable-migration',
@@ -19,7 +19,7 @@ export class WorkspaceCustomApplicationIdNonNullableCommand extends ActiveOrSusp
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -12,7 +12,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Command({
@@ -30,7 +30,7 @@ export class BackfillPageLayoutUniversalIdentifiersCommand extends ActiveOrSuspe
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
@InjectRepository(PageLayoutEntity)
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
@@ -11,7 +11,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.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';
@Command({
name: 'upgrade:1-13:backfill:view-main-group-by-field-metadata-id',
@@ -26,7 +26,7 @@ export class BackfillViewMainGroupByFieldMetadataIdCommand extends ActiveOrSuspe
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
@@ -6,14 +6,15 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@@ -30,14 +31,14 @@ export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWo
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(ObjectMetadataEntity)
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
protected readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
@@ -113,7 +114,7 @@ export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWo
objectMetadataItem: ObjectMetadataEntity,
tableName: string,
schemaName: string,
dataSource: WorkspaceDataSource,
dataSource: WorkspaceDataSourceInterface,
isDryRun: boolean,
): Promise<void> {
const textFields = objectMetadataItem.fields.filter(
@@ -154,7 +155,7 @@ export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWo
objectMetadataItem: ObjectMetadataEntity,
tableName: string,
schemaName: string,
dataSource: WorkspaceDataSource,
dataSource: WorkspaceDataSourceInterface,
isDryRun: boolean,
): Promise<void> {
const nameField = objectMetadataItem.fields.find(
@@ -8,7 +8,7 @@ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/worksp
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.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';
type DuplicateKey = 'apiKeyId' | 'agentId' | 'userWorkspaceId';
@@ -28,7 +28,7 @@ export class DeduplicateRoleTargetsCommand extends ActiveOrSuspendedWorkspacesMi
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
@@ -20,7 +20,7 @@ import { MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { 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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
import { AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
@@ -263,7 +263,7 @@ export class MigrateStandardInvalidEntitiesCommand extends ActiveOrSuspendedWork
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fieldMetadataService: FieldMetadataService,
@@ -13,7 +13,7 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@@ -27,7 +27,7 @@ export class RenameIndexNameCommand extends ActiveOrSuspendedWorkspacesMigration
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly workspaceCacheService: WorkspaceCacheService,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
@@ -7,7 +7,7 @@ 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';
@Command({
name: 'upgrade:1-13:update-role-targets-unique-constraint-migration',
@@ -20,7 +20,7 @@ export class UpdateRoleTargetsUniqueConstraintMigrationCommand extends ActiveOrS
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -9,7 +9,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.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';
@Command({
name: 'upgrade:1-6:fix-label-identifier-position-and-visibility',
@@ -20,7 +20,7 @@ export class FixLabelIdentifierPositionAndVisibilityCommand extends ActiveOrSusp
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@@ -8,7 +8,7 @@ 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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
@@ -21,7 +21,7 @@ export class BackfillWorkflowManualTriggerAvailabilityCommand extends ActiveOrSu
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@@ -5,6 +5,8 @@ import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Not, Repository } from 'typeorm';
import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -22,8 +24,7 @@ import {
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
@@ -37,7 +38,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly indexMetadataService: IndexMetadataService,
protected readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
@@ -188,7 +189,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
dataSource,
dryRun,
}: {
dataSource: WorkspaceDataSource;
dataSource: WorkspaceDataSourceInterface;
dryRun: boolean;
}) {
const workspaceMemberRepository = dataSource.getRepository(
@@ -248,7 +249,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
dataSource,
dryRun,
}: {
dataSource: WorkspaceDataSource;
dataSource: WorkspaceDataSourceInterface;
dryRun: boolean;
}) {
const companyRepository = dataSource.getRepository('company', {
@@ -308,7 +309,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
dataSource,
dryRun,
}: {
dataSource: WorkspaceDataSource;
dataSource: WorkspaceDataSourceInterface;
dryRun: boolean;
}) {
const personRepository = dataSource.getRepository('person', {
@@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.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';
@Command({
name: 'upgrade:1-8:fill-null-serverless-function-layer-id',
@@ -24,7 +24,7 @@ export class FillNullServerlessFunctionLayerIdCommand extends ActiveOrSuspendedW
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ServerlessFunctionEntity)
protected readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@@ -11,7 +11,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
@@ -30,7 +30,7 @@ export class MigrateChannelSyncStagesCommand extends ActiveOrSuspendedWorkspaces
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@@ -12,7 +12,7 @@ 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 { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard';
import { isWorkflowFindRecordsAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-find-records-action.guard';
@@ -26,7 +26,7 @@ export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspen
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
@@ -43,7 +43,7 @@ export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspen
);
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
@@ -7,7 +7,7 @@ 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 { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
@@ -24,7 +24,7 @@ export class RegeneratePersonSearchVectorWithPhonesCommand extends ActiveOrSuspe
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
) {
@@ -41,7 +41,7 @@ import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/comm
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
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 { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
@Command({
@@ -55,7 +55,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyConfigService: TwentyConfigService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
@@ -106,7 +106,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
super(
workspaceRepository,
twentyConfigService,
twentyORMGlobalManager,
globalWorkspaceOrmManager,
dataSourceService,
syncWorkspaceMetadataCommand,
);
@@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { Omit } from 'zod/v4/core/util.cjs';
import { type PermissionFlagType } from 'twenty-shared/constants';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
@@ -50,7 +50,6 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Injectable()
@@ -65,8 +64,6 @@ export abstract class CommonBaseQueryRunnerService<
@Inject()
protected readonly dataArgProcessor: DataArgProcessor;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
@Inject()
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
@@ -23,7 +23,6 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module';
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
@@ -42,7 +41,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
MetricsModule,
RecordPositionModule,
RecordTransformerModule,
GlobalWorkspaceDataSourceModule,
FeatureFlagModule,
WorkspaceCacheModule,
],
@@ -33,7 +33,6 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
@@ -55,8 +54,6 @@ export interface FormatResult {
}
export abstract class RestApiBaseHandler {
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
protected readonly workspaceCacheService: WorkspaceCacheService;
@Inject()
@@ -14,7 +14,7 @@ import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.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 WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
type TestingAuthContext = Omit<AuthContext, 'workspace' | 'apiKey' | 'user'> & {
@@ -36,12 +36,16 @@ describe('CreatedByFromAuthContextService', () => {
const mockWorkspaceMemberRepository = {
findOneOrFail: jest.fn(),
};
const twentyORMGlobalManager: jest.Mocked<
Pick<TwentyORMGlobalManager, 'getRepositoryForWorkspace'>
const globalWorkspaceOrmManager: jest.Mocked<
Pick<
GlobalWorkspaceOrmManager,
'getRepository' | 'executeInWorkspaceContext'
>
> = {
getRepositoryForWorkspace: jest
getRepository: jest.fn().mockResolvedValue(mockWorkspaceMemberRepository),
executeInWorkspaceContext: jest
.fn()
.mockResolvedValue(mockWorkspaceMemberRepository),
.mockImplementation((_authContext: any, fn: () => any) => fn()),
};
beforeEach(async () => {
@@ -49,8 +53,8 @@ describe('CreatedByFromAuthContextService', () => {
providers: [
CreatedByFromAuthContextService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
{
provide: getRepositoryToken(FieldMetadataEntity),
@@ -3,6 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { type ActorMetadata } from 'twenty-shared/types';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -10,7 +12,7 @@ import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -21,7 +23,7 @@ export class CreatedByFromAuthContextService {
private readonly logger = new Logger(CreatedByFromAuthContextService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -109,23 +111,30 @@ export class CreatedByFromAuthContextService {
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
if (isDefined(user)) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext as WorkspaceAuthContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
{
where: {
userId: user.id,
},
},
);
return buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
});
},
});
return buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
});
);
}
if (isDefined(apiKey)) {
@@ -17,7 +17,8 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@UseGuards(
@@ -32,7 +33,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Resolver()
export class ApprovedAccessDomainResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
) {}
@@ -42,18 +43,26 @@ export class ApprovedAccessDomainResolver {
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
@AuthUser() currentUser: UserEntity,
): Promise<ApprovedAccessDomainDTO> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
currentWorkspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(currentWorkspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: currentUser.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
currentWorkspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: currentUser.id,
},
});
},
);
return this.approvedAccessDomainService.createApprovedAccessDomain(
domain,
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
@@ -22,7 +23,7 @@ export type CreateCalendarChannelInput = {
@Injectable()
export class CreateCalendarChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async createCalendarChannel(
@@ -36,26 +37,33 @@ export class CreateCalendarChannelService {
manager,
} = input;
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const authContext = buildSystemAuthContext(workspaceId);
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId,
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
syncStatus: CalendarChannelSyncStatus.NOT_SYNCED,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId,
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
syncStatus: CalendarChannelSyncStatus.NOT_SYNCED,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
{},
manager,
);
return newCalendarChannel.id;
},
{},
manager,
);
return newCalendarChannel.id;
}
}
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
import { type ConnectedAccountProvider } from 'twenty-shared/types';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type CreateConnectedAccountInput = {
@@ -21,7 +22,7 @@ export type CreateConnectedAccountInput = {
@Injectable()
export class CreateConnectedAccountService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async createConnectedAccount(
@@ -39,24 +40,31 @@ export class CreateConnectedAccountService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
await connectedAccountRepository.save(
{
id: connectedAccountId,
handle,
provider,
accessToken,
refreshToken,
accountOwnerId,
scopes,
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.save(
{
id: connectedAccountId,
handle,
provider,
accessToken,
refreshToken,
accountOwnerId,
scopes,
},
{},
manager,
);
},
{},
manager,
);
}
}
@@ -4,7 +4,8 @@ import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelPendingGroupEmailsAction,
@@ -27,7 +28,7 @@ export type CreateMessageChannelInput = {
@Injectable()
export class CreateMessageChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
) {}
@@ -42,49 +43,57 @@ export class CreateMessageChannelService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
});
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction:
MessageChannelPendingGroupEmailsAction.NONE,
},
{},
manager,
);
if (isDefined(connectedAccount)) {
await this.syncMessageFoldersService.syncMessageFolders({
workspaceId,
messageChannel: {
...newMessageChannel,
connectedAccount,
},
manager,
});
}
return newMessageChannel.id;
},
{},
manager,
);
if (isDefined(connectedAccount)) {
await this.syncMessageFoldersService.syncMessageFolders({
workspaceId,
messageChannel: {
...newMessageChannel,
connectedAccount,
},
manager,
});
}
return newMessageChannel.id;
}
}
@@ -13,7 +13,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
@@ -77,9 +77,9 @@ describe('GoogleAPIsService', () => {
providers: [
GoogleAPIsService,
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity) => {
if (entity === 'connectedAccount')
@@ -96,6 +96,9 @@ describe('GoogleAPIsService', () => {
getDataSourceForWorkspace: jest
.fn()
.mockImplementation(() => mockWorkspaceDataSource),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -17,7 +17,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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
@@ -45,7 +46,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Injectable()
export class GoogleAPIsService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
@@ -82,36 +83,6 @@ export class GoogleAPIsService {
'CALENDAR_PROVIDER_GOOGLE_ENABLED',
);
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const { scopes, isValid } =
await this.googleAPIScopesService.getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
input.accessToken,
@@ -124,131 +95,167 @@ export class GoogleAPIsService {
);
}
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
const authContext = buildSystemAuthContext(workspaceId);
await this.createMessageChannelService.createMessageChannel({
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
'connectedAccount',
);
if (isCalendarEnabled) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getDataSourceForWorkspace(
workspaceId,
);
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
if (isCalendarEnabled) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember =
await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
});
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
},
);
if (this.twentyConfigService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
{
where: { id: workspaceMemberId },
},
);
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
if (isCalendarEnabled) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
},
);
if (this.twentyConfigService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
}
if (isCalendarEnabled) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
}
}
@@ -12,7 +12,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
@@ -76,9 +76,9 @@ describe('MicrosoftAPIsService', () => {
providers: [
MicrosoftAPIsService,
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity) => {
if (entity === 'connectedAccount')
@@ -95,6 +95,9 @@ describe('MicrosoftAPIsService', () => {
getDataSourceForWorkspace: jest
.fn()
.mockImplementation(() => mockWorkspaceDataSource),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -13,7 +13,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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
@@ -41,7 +42,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Injectable()
export class MicrosoftAPIsService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
@@ -73,170 +74,177 @@ export class MicrosoftAPIsService {
messageVisibility,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const scopes = getMicrosoftApisOauthScopes();
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
const authContext = buildSystemAuthContext(workspaceId);
await this.createMessageChannelService.createMessageChannel({
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
'connectedAccount',
);
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getDataSourceForWorkspace(
workspaceId,
);
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
if (
this.twentyConfigService.get(
'CALENDAR_PROVIDER_MICROSOFT_ENABLED',
)
) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember =
await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
});
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
},
);
if (
this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')
) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
{
where: { id: workspaceMemberId },
},
);
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
},
);
if (this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
}
if (this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
}
}
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type UpdateConnectedAccountOnReconnectInput = {
@@ -17,7 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = {
@Injectable()
export class UpdateConnectedAccountOnReconnectService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async updateConnectedAccountOnReconnect(
@@ -32,23 +33,30 @@ export class UpdateConnectedAccountOnReconnectService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
await connectedAccountRepository.update(
{
id: connectedAccountId,
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.update(
{
id: connectedAccountId,
},
{
accessToken,
refreshToken,
scopes,
authFailedAt: null,
},
manager,
);
},
{
accessToken,
refreshToken,
scopes,
authFailedAt: null,
},
manager,
);
}
}
@@ -17,7 +17,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
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 { AccessTokenService } from './access-token.service';
@@ -27,7 +27,7 @@ describe('AccessTokenService', () => {
let twentyConfigService: TwentyConfigService;
let userRepository: Repository<UserEntity>;
let workspaceRepository: Repository<WorkspaceEntity>;
let twentyORMGlobalManager: TwentyORMGlobalManager;
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let userWorkspaceRepository: Repository<UserWorkspaceEntity>;
beforeEach(async () => {
@@ -77,9 +77,12 @@ describe('AccessTokenService', () => {
useValue: {},
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
getRepository: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
],
@@ -94,8 +97,8 @@ describe('AccessTokenService', () => {
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
getRepositoryToken(WorkspaceEntity),
);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
userWorkspaceRepository = module.get<Repository<UserWorkspaceEntity>>(
getRepositoryToken(UserWorkspaceEntity),
@@ -131,11 +134,9 @@ describe('AccessTokenService', () => {
jest
.spyOn(userWorkspaceRepository, 'findOne')
.mockResolvedValue(mockUserWorkspace as UserWorkspaceEntity);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
jest.spyOn(globalWorkspaceOrmManager, 'getRepository').mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken);
const result = await service.generateAccessToken({
@@ -192,11 +193,9 @@ describe('AccessTokenService', () => {
id: impersonatedUserWorkspaceId,
workspaceId,
} as UserWorkspaceEntity);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
jest.spyOn(globalWorkspaceOrmManager, 'getRepository').mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
const signSpy = jest
.spyOn(jwtWrapperService, 'sign')
.mockReturnValue(mockToken);
@@ -28,7 +28,8 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
@@ -41,7 +42,7 @@ export class AccessTokenService {
private readonly userRepository: Repository<UserEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
@@ -79,31 +80,39 @@ export class AccessTokenService {
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
if (isWorkspaceActiveOrSuspended(workspace)) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
tokenWorkspaceMemberId =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
assertIsDefinedOrThrow(
workspaceMember,
new AuthException(
'User is not a member of the workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
{
userFriendlyMessage: msg`User is not a member of the workspace.`,
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
assertIsDefinedOrThrow(
workspaceMember,
new AuthException(
'User is not a member of the workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
{
userFriendlyMessage: msg`User is not a member of the workspace.`,
},
),
);
return workspaceMember.id;
},
),
);
tokenWorkspaceMemberId = workspaceMember.id;
);
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
@@ -12,7 +12,7 @@ import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
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';
@Command({
name: 'billing:sync-customer-data',
@@ -25,10 +25,10 @@ export class BillingSyncCustomerDataCommand extends ActiveOrSuspendedWorkspacesM
private readonly stripeSubscriptionService: StripeSubscriptionService,
@InjectRepository(BillingCustomerEntity)
protected readonly billingCustomerRepository: Repository<BillingCustomerEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
@@ -13,7 +13,7 @@ import { BillingSubscriptionService } from 'src/engine/core-modules/billing/serv
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
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';
@Command({
name: 'billing:update-subscription-price',
@@ -27,14 +27,14 @@ export class BillingUpdateSubscriptionPriceCommand extends ActiveOrSuspendedWork
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(BillingSubscriptionEntity)
protected readonly billingSubscriptionRepository: Repository<BillingSubscriptionEntity>,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
@Option({
@@ -7,7 +7,8 @@ import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/s
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
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 { 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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type UpdateSubscriptionQuantityJobData = { workspaceId: string };
@@ -21,41 +22,48 @@ export class UpdateSubscriptionQuantityJob {
constructor(
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
@Process(UpdateSubscriptionQuantityJob.name)
async handle(data: UpdateSubscriptionQuantityJobData): Promise<void> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
data.workspaceId,
'workspaceMember',
);
const authContext = buildSystemAuthContext(data.workspaceId);
const workspaceMembersCount = await workspaceMemberRepository.count();
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
data.workspaceId,
'workspaceMember',
);
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
return;
}
const workspaceMembersCount = await workspaceMemberRepository.count();
try {
const billingBaseProductSubscriptionItem =
await this.billingSubscriptionService.getBaseProductCurrentBillingSubscriptionItemOrThrow(
data.workspaceId,
);
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
return;
}
await this.stripeSubscriptionItemService.updateSubscriptionItem(
billingBaseProductSubscriptionItem.stripeSubscriptionItemId,
{ quantity: workspaceMembersCount },
);
try {
const billingBaseProductSubscriptionItem =
await this.billingSubscriptionService.getBaseProductCurrentBillingSubscriptionItemOrThrow(
data.workspaceId,
);
this.logger.log(
`Updating workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members`,
);
} catch (e) {
this.logger.warn(
`Failed to update workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members. Error: ${e}`,
);
}
await this.stripeSubscriptionItemService.updateSubscriptionItem(
billingBaseProductSubscriptionItem.stripeSubscriptionItemId,
{ quantity: workspaceMembersCount },
);
this.logger.log(
`Updating workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members`,
);
} catch (e) {
this.logger.warn(
`Failed to update workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members. Error: ${e}`,
);
}
},
);
}
}
@@ -2,8 +2,8 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
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 { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
@@ -36,18 +36,19 @@ describe('TimelineCalendarEventService', () => {
findAndCount: jest.fn(),
};
const mockTwentyORMGlobalManager = {
getRepositoryForWorkspace: jest
const mockGlobalWorkspaceOrmManager = {
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
executeInWorkspaceContext: jest
.fn()
.mockResolvedValue(mockCalendarEventRepository),
.mockImplementation((_authContext: any, fn: () => any) => fn()),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TimelineCalendarEventService,
{
provide: TwentyORMGlobalManager,
useValue: mockTwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: mockGlobalWorkspaceOrmManager,
},
],
}).compile();
@@ -6,7 +6,8 @@ import { Any } from 'typeorm';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.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 { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
@@ -15,10 +16,9 @@ import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/
@Injectable()
export class TimelineCalendarEventService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
// TODO: Align return type with the entities to avoid mapping
async getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds,
@@ -32,136 +32,141 @@ export class TimelineCalendarEventService {
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const offset = (page - 1) * pageSize;
const authContext = buildSystemAuthContext(workspaceId);
const calendarEventRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarEventWorkspaceEntity>(
workspaceId,
'calendarEvent',
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const offset = (page - 1) * pageSize;
const calendarEventIds = await calendarEventRepository.find({
where: {
calendarEventParticipants: {
personId: Any(personIds),
},
},
select: {
id: true,
startsAt: true,
},
skip: offset,
take: pageSize,
order: {
startsAt: 'DESC',
},
});
const calendarEventRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarEventWorkspaceEntity>(
workspaceId,
'calendarEvent',
);
const ids = calendarEventIds.map(({ id }) => id);
if (ids.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
// We've split the query into two parts, because we want to fetch all the participants without any filtering
const [events, total] = await calendarEventRepository.findAndCount({
where: {
id: Any(ids),
},
relations: {
calendarEventParticipants: {
person: true,
workspaceMember: true,
},
calendarChannelEventAssociations: {
calendarChannel: {
connectedAccount: {
accountOwner: true,
const calendarEventIds = await calendarEventRepository.find({
where: {
calendarEventParticipants: {
personId: Any(personIds),
},
},
},
},
});
select: {
id: true,
startsAt: true,
},
skip: offset,
take: pageSize,
order: {
startsAt: 'DESC',
},
});
// Keep events in the same order as they ids were returned
const orderedEvents = events.sort(
(a, b) => ids.indexOf(a.id) - ids.indexOf(b.id),
);
const ids = calendarEventIds.map(({ id }) => id);
const timelineCalendarEvents = orderedEvents.map((event) => {
const participants = event.calendarEventParticipants.map(
(participant) => ({
calendarEventId: event.id,
personId: participant.personId ?? null,
workspaceMemberId: participant.workspaceMemberId ?? null,
firstName:
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
'',
lastName:
participant.person?.name?.lastName ||
participant.workspaceMember?.name.lastName ||
'',
displayName:
participant.person?.name?.firstName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.firstName ||
participant.workspaceMember?.name.lastName ||
participant.displayName ||
participant.handle ||
'',
avatarUrl:
participant.person?.avatarUrl ||
participant.workspaceMember?.avatarUrl ||
'',
handle: participant.handle ?? '',
}),
);
if (ids.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const isCalendarEventImportedByCurrentWorkspaceMember =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.connectedAccount.accountOwnerId ===
currentWorkspaceMemberId,
const [events, total] = await calendarEventRepository.findAndCount({
where: {
id: Any(ids),
},
relations: {
calendarEventParticipants: {
person: true,
workspaceMember: true,
},
calendarChannelEventAssociations: {
calendarChannel: {
connectedAccount: {
accountOwner: true,
},
},
},
},
});
const orderedEvents = events.sort(
(a, b) => ids.indexOf(a.id) - ids.indexOf(b.id),
);
const visibility =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
) || isCalendarEventImportedByCurrentWorkspaceMember
? CalendarChannelVisibility.SHARE_EVERYTHING
: CalendarChannelVisibility.METADATA;
const timelineCalendarEvents = orderedEvents.map((event) => {
const participants = event.calendarEventParticipants.map(
(participant) => ({
calendarEventId: event.id,
personId: participant.personId ?? null,
workspaceMemberId: participant.workspaceMemberId ?? null,
firstName:
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
'',
lastName:
participant.person?.name?.lastName ||
participant.workspaceMember?.name.lastName ||
'',
displayName:
participant.person?.name?.firstName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.firstName ||
participant.workspaceMember?.name.lastName ||
participant.displayName ||
participant.handle ||
'',
avatarUrl:
participant.person?.avatarUrl ||
participant.workspaceMember?.avatarUrl ||
'',
handle: participant.handle ?? '',
}),
);
return {
...omit(event, [
'calendarEventParticipants',
'calendarChannelEventAssociations',
]),
title:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.title ?? ''),
description:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.description ?? ''),
startsAt: event.startsAt as unknown as Date,
endsAt: event.endsAt as unknown as Date,
participants,
visibility,
location: event.location ?? '',
conferenceSolution: event.conferenceSolution ?? '',
};
});
const isCalendarEventImportedByCurrentWorkspaceMember =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.connectedAccount.accountOwnerId ===
currentWorkspaceMemberId,
);
return {
totalNumberOfCalendarEvents: total,
timelineCalendarEvents,
};
const visibility =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
) || isCalendarEventImportedByCurrentWorkspaceMember
? CalendarChannelVisibility.SHARE_EVERYTHING
: CalendarChannelVisibility.METADATA;
return {
...omit(event, [
'calendarEventParticipants',
'calendarChannelEventAssociations',
]),
title:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.title ?? ''),
description:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.description ?? ''),
startsAt: event.startsAt as unknown as Date,
endsAt: event.endsAt as unknown as Date,
participants,
visibility,
location: event.location ?? '',
conferenceSolution: event.conferenceSolution ?? '',
};
});
return {
totalNumberOfCalendarEvents: total,
timelineCalendarEvents,
};
},
);
}
async getCalendarEventsFromCompanyId({
@@ -177,39 +182,46 @@ export class TimelineCalendarEventService {
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const personRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const authContext = buildSystemAuthContext(workspaceId);
const personIds = await personRepository.find({
where: {
companyId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const personRepository =
await this.globalWorkspaceOrmManager.getRepository<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = await personRepository.find({
where: {
companyId,
},
select: {
id: true,
},
});
if (personIds.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const formattedPersonIds = personIds.map(({ id }) => id);
const calendarEvents = await this.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds: formattedPersonIds,
workspaceId,
page,
pageSize,
});
return calendarEvents;
},
select: {
id: true,
},
});
if (personIds.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const formattedPersonIds = personIds.map(({ id }) => id);
const calendarEvents = await this.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds: formattedPersonIds,
workspaceId,
page,
pageSize,
});
return calendarEvents;
);
}
async getCalendarEventsFromOpportunityId({
@@ -225,36 +237,43 @@ export class TimelineCalendarEventService {
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const authContext = buildSystemAuthContext(workspaceId);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const opportunityRepository =
await this.globalWorkspaceOrmManager.getRepository<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const calendarEvents = await this.getCalendarEventsFromCompanyId({
currentWorkspaceMemberId,
companyId: opportunity.companyId,
workspaceId,
page,
pageSize,
});
return calendarEvents;
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const calendarEvents = await this.getCalendarEventsFromCompanyId({
currentWorkspaceMemberId,
companyId: opportunity.companyId,
workspaceId,
page,
pageSize,
});
return calendarEvents;
);
}
}
@@ -7,9 +7,8 @@ import {
} from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
@@ -27,9 +26,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Resolver()
@UsePipes(ResolverValidationPipe)
@@ -37,7 +34,6 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKSPACE))
export class ImapSmtpCaldavResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly ImapSmtpCaldavConnectionService: ImapSmtpCaldavService,
private readonly imapSmtpCaldavApisService: ImapSmtpCalDavAPIService,
private readonly featureFlagService: FeatureFlagService,
@@ -50,16 +46,12 @@ export class ImapSmtpCaldavResolver {
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ConnectedImapSmtpCaldavAccountDTO> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
const connectedAccount =
await this.imapSmtpCaldavApisService.getImapSmtpCaldavConnectedAccount(
workspace.id,
'connectedAccount',
id,
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV },
});
if (!isDefined(connectedAccount) || !isDefined(connectedAccount?.handle)) {
throw new UserInputError(
`Connected mail account with ID ${id} not found`,
@@ -10,7 +10,8 @@ import {
type AccountType,
type ConnectionParameters,
} from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.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 { CalDAVClient } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -19,7 +20,7 @@ export class ImapSmtpCaldavService {
private readonly logger = new Logger(ImapSmtpCaldavService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async testImapConnection(
@@ -175,19 +176,26 @@ export class ImapSmtpCaldavService {
workspaceId: string,
connectionId: string,
): Promise<ConnectedAccountWorkspaceEntity | null> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
const connectedAccount = await connectedAccountRepository.findOne({
where: {
id: connectionId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: {
id: connectionId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
});
return connectedAccount;
},
});
return connectedAccount;
);
}
}
@@ -4,14 +4,15 @@ import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/mess
import { type TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util';
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 OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@Injectable()
export class GetMessagesService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly timelineMessagingService: TimelineMessagingService,
) {}
@@ -73,38 +74,45 @@ export class GetMessagesService {
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
const personRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = (
await personRepository.find({
where: {
companyId,
},
select: {
id: true,
},
})
).map((person) => person.id);
const authContext = buildSystemAuthContext(workspaceId);
if (personIds.length === 0) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const personRepository =
await this.globalWorkspaceOrmManager.getRepository<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = (
await personRepository.find({
where: {
companyId,
},
select: {
id: true,
},
})
).map((person) => person.id);
const messageThreads = await this.getMessagesFromPersonIds(
workspaceMemberId,
personIds,
workspaceId,
page,
pageSize,
if (personIds.length === 0) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
const messageThreads = await this.getMessagesFromPersonIds(
workspaceMemberId,
personIds,
workspaceId,
page,
pageSize,
);
return messageThreads;
},
);
return messageThreads;
}
async getMessagesFromOpportunityId(
@@ -114,36 +122,43 @@ export class GetMessagesService {
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const authContext = buildSystemAuthContext(workspaceId);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const opportunityRepository =
await this.globalWorkspaceOrmManager.getRepository<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
const messageThreads = await this.getMessagesFromCompanyId(
workspaceMemberId,
opportunity.companyId,
workspaceId,
page,
pageSize,
);
return messageThreads;
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
const messageThreads = await this.getMessagesFromCompanyId(
workspaceMemberId,
opportunity.companyId,
workspaceId,
page,
pageSize,
);
return messageThreads;
}
}
@@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { MessageParticipantRole } from 'twenty-shared/types';
import { In } from 'typeorm';
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.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 { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
@@ -12,7 +13,7 @@ import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/
@Injectable()
export class TimelineMessagingService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
public async getAndCountMessageThreads(
@@ -31,63 +32,74 @@ export class TimelineMessagingService {
>[];
totalNumberOfThreads: number;
}> {
const messageThreadRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const authContext = buildSystemAuthContext(workspaceId);
const totalNumberOfThreads = await messageThreadRepository
.createQueryBuilder('messageThread')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN(:...personIds)', { personIds })
.groupBy('messageThread.id')
.getCount();
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const messageThreadRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const threadIdsQuery = await messageThreadRepository
.createQueryBuilder('messageThread')
.select('messageThread.id', 'id')
.addSelect('MAX(messages.receivedAt)', 'max_received_at')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN (:...personIds)', { personIds })
.groupBy('messageThread.id')
.orderBy('max_received_at', 'DESC')
.offset(offset)
.limit(pageSize)
.getRawMany();
const totalNumberOfThreads = await messageThreadRepository
.createQueryBuilder('messageThread')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN(:...personIds)', {
personIds,
})
.groupBy('messageThread.id')
.getCount();
const messageThreadIds = threadIdsQuery.map((thread) => thread.id);
const threadIdsQuery = await messageThreadRepository
.createQueryBuilder('messageThread')
.select('messageThread.id', 'id')
.addSelect('MAX(messages.receivedAt)', 'max_received_at')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN (:...personIds)', {
personIds,
})
.groupBy('messageThread.id')
.orderBy('max_received_at', 'DESC')
.offset(offset)
.limit(pageSize)
.getRawMany();
const messageThreads = await messageThreadRepository.find({
where: {
id: In(messageThreadIds),
},
order: {
messages: {
receivedAt: 'DESC',
},
},
relations: ['messages'],
});
const messageThreadIds = threadIdsQuery.map((thread) => thread.id);
return {
messageThreads: messageThreads.map((messageThread) => {
const lastMessage = messageThread.messages[0];
const firstMessage =
messageThread.messages[messageThread.messages.length - 1];
const messageThreads = await messageThreadRepository.find({
where: {
id: In(messageThreadIds),
},
order: {
messages: {
receivedAt: 'DESC',
},
},
relations: ['messages'],
});
return {
id: messageThread.id,
subject: firstMessage.subject ?? '',
lastMessageBody: lastMessage.text ?? '',
lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(),
numberOfMessagesInThread: messageThread.messages.length,
messageThreads: messageThreads.map((messageThread) => {
const lastMessage = messageThread.messages[0];
const firstMessage =
messageThread.messages[messageThread.messages.length - 1];
return {
id: messageThread.id,
subject: firstMessage.subject ?? '',
lastMessageBody: lastMessage.text ?? '',
lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(),
numberOfMessagesInThread: messageThread.messages.length,
};
}),
totalNumberOfThreads,
};
}),
totalNumberOfThreads,
};
},
);
}
public async getThreadParticipantsByThreadId(
@@ -96,89 +108,96 @@ export class TimelineMessagingService {
): Promise<{
[key: string]: MessageParticipantWorkspaceEntity[];
}> {
const messageParticipantRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageParticipantWorkspaceEntity>(
workspaceId,
'messageParticipant',
);
const authContext = buildSystemAuthContext(workspaceId);
const threadParticipants = await messageParticipantRepository
.createQueryBuilder()
.select('messageParticipant')
.addSelect('message.messageThreadId')
.addSelect('message.receivedAt')
.leftJoinAndSelect('messageParticipant.person', 'person')
.leftJoinAndSelect(
'messageParticipant.workspaceMember',
'workspaceMember',
)
.leftJoin('messageParticipant.message', 'message')
.where('message.messageThreadId = ANY(:messageThreadIds)', {
messageThreadIds,
})
.andWhere('messageParticipant.role = :role', {
role: MessageParticipantRole.FROM,
})
.orderBy('message.messageThreadId')
.distinctOn(['message.messageThreadId', 'messageParticipant.handle'])
.getMany();
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const messageParticipantRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageParticipantWorkspaceEntity>(
workspaceId,
'messageParticipant',
);
// This is because subqueries are not handled by twentyORM
const orderedThreadParticipants = threadParticipants.sort(
(a, b) =>
(a.message.receivedAt ?? new Date()).getTime() -
(b.message.receivedAt ?? new Date()).getTime(),
);
const threadParticipants = await messageParticipantRepository
.createQueryBuilder()
.select('messageParticipant')
.addSelect('message.messageThreadId')
.addSelect('message.receivedAt')
.leftJoinAndSelect('messageParticipant.person', 'person')
.leftJoinAndSelect(
'messageParticipant.workspaceMember',
'workspaceMember',
)
.leftJoin('messageParticipant.message', 'message')
.where('message.messageThreadId = ANY(:messageThreadIds)', {
messageThreadIds,
})
.andWhere('messageParticipant.role = :role', {
role: MessageParticipantRole.FROM,
})
.orderBy('message.messageThreadId')
.distinctOn(['message.messageThreadId', 'messageParticipant.handle'])
.getMany();
// This is because composite fields are not handled correctly by the ORM
const threadParticipantsWithCompositeFields = orderedThreadParticipants.map(
(threadParticipant) => ({
...threadParticipant,
person: {
id: threadParticipant.person?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.person?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.person?.nameLastName,
},
avatarUrl: threadParticipant.person?.avatarUrl,
},
workspaceMember: {
id: threadParticipant.workspaceMember?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.workspaceMember?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.workspaceMember?.nameLastName,
},
avatarUrl: threadParticipant.workspaceMember?.avatarUrl,
},
}),
);
return threadParticipantsWithCompositeFields.reduce(
(threadParticipantsAcc, threadParticipant) => {
if (!threadParticipant.message.messageThreadId)
return threadParticipantsAcc;
// @ts-expect-error legacy noImplicitAny
if (!threadParticipantsAcc[threadParticipant.message.messageThreadId])
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[threadParticipant.message.messageThreadId] = [];
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[threadParticipant.message.messageThreadId].push(
threadParticipant,
const orderedThreadParticipants = threadParticipants.sort(
(a, b) =>
(a.message.receivedAt ?? new Date()).getTime() -
(b.message.receivedAt ?? new Date()).getTime(),
);
return threadParticipantsAcc;
const threadParticipantsWithCompositeFields =
orderedThreadParticipants.map((threadParticipant) => ({
...threadParticipant,
person: {
id: threadParticipant.person?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.person?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.person?.nameLastName,
},
avatarUrl: threadParticipant.person?.avatarUrl,
},
workspaceMember: {
id: threadParticipant.workspaceMember?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.workspaceMember?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.workspaceMember?.nameLastName,
},
avatarUrl: threadParticipant.workspaceMember?.avatarUrl,
},
}));
return threadParticipantsWithCompositeFields.reduce(
(threadParticipantsAcc, threadParticipant) => {
if (!threadParticipant.message.messageThreadId)
return threadParticipantsAcc;
if (
// @ts-expect-error legacy noImplicitAny
!threadParticipantsAcc[threadParticipant.message.messageThreadId]
)
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[threadParticipant.message.messageThreadId] =
[];
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[
threadParticipant.message.messageThreadId
].push(threadParticipant);
return threadParticipantsAcc;
},
{},
);
},
{},
);
}
@@ -189,58 +208,65 @@ export class TimelineMessagingService {
): Promise<{
[key: string]: MessageChannelVisibility;
}> {
const messageThreadRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const authContext = buildSystemAuthContext(workspaceId);
const threadVisibility = await messageThreadRepository
.createQueryBuilder()
.select('messageThread.id', 'id')
.addSelect('messageChannel.visibility', 'visibility')
.addSelect('connectedAccount.accountOwnerId', 'accountOwnerId')
.leftJoin('messageThread.messages', 'message')
.leftJoin(
'message.messageChannelMessageAssociations',
'messageChannelMessageAssociation',
)
.leftJoin(
'messageChannelMessageAssociation.messageChannel',
'messageChannel',
)
.leftJoin('messageChannel.connectedAccount', 'connectedAccount')
.where('messageThread.id = ANY(:messageThreadIds)', {
messageThreadIds: messageThreadIds,
})
.getRawMany();
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const messageThreadRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const visibilityValues = Object.values(MessageChannelVisibility);
const threadVisibilityByThreadId: {
[key: string]: MessageChannelVisibility;
} = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => {
if (threadVisibility.accountOwnerId === workspaceMemberId) {
threadVisibilityAcc[threadVisibility.id] =
MessageChannelVisibility.SHARE_EVERYTHING;
return threadVisibilityAcc;
}
threadVisibilityAcc[threadVisibility.id] =
visibilityValues[
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
const threadVisibility = await messageThreadRepository
.createQueryBuilder()
.select('messageThread.id', 'id')
.addSelect('messageChannel.visibility', 'visibility')
.addSelect('connectedAccount.accountOwnerId', 'accountOwnerId')
.leftJoin('messageThread.messages', 'message')
.leftJoin(
'message.messageChannelMessageAssociations',
'messageChannelMessageAssociation',
)
];
.leftJoin(
'messageChannelMessageAssociation.messageChannel',
'messageChannel',
)
.leftJoin('messageChannel.connectedAccount', 'connectedAccount')
.where('messageThread.id = ANY(:messageThreadIds)', {
messageThreadIds: messageThreadIds,
})
.getRawMany();
return threadVisibilityAcc;
}, {});
const visibilityValues = Object.values(MessageChannelVisibility);
return threadVisibilityByThreadId;
const threadVisibilityByThreadId: {
[key: string]: MessageChannelVisibility;
} = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => {
if (threadVisibility.accountOwnerId === workspaceMemberId) {
threadVisibilityAcc[threadVisibility.id] =
MessageChannelVisibility.SHARE_EVERYTHING;
return threadVisibilityAcc;
}
threadVisibilityAcc[threadVisibility.id] =
visibilityValues[
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
)
];
return threadVisibilityAcc;
}, {});
return threadVisibilityByThreadId;
},
);
}
}
@@ -16,14 +16,15 @@ import { RecordInputTransformerService } from 'src/engine/core-modules/record-tr
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
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';
@Injectable()
export class CreateRecordService {
private readonly logger = new Logger(CreateRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordPositionService: RecordPositionService,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
@@ -40,116 +41,124 @@ export class CreateRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to create: Object cannot be created by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to create: Object cannot be created by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: flatObjectMetadata,
workspaceId,
});
const position = await this.recordPositionService.buildRecordPosition(
{
value: 'first',
objectMetadata: flatObjectMetadata,
workspaceId,
},
);
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const validObjectRecord = Object.fromEntries(
Object.entries(objectRecord).filter(
([key]) =>
isDefined(fieldIdByName[key]) ||
isDefined(fieldIdByJoinColumnName[key]),
),
);
const validObjectRecord = Object.fromEntries(
Object.entries(objectRecord).filter(
([key]) =>
isDefined(fieldIdByName[key]) ||
isDefined(fieldIdByJoinColumnName[key]),
),
);
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: validObjectRecord,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const insertResult = await repository.insert(
{
...transformedObjectRecord,
position,
createdBy: params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
},
},
undefined,
selectedColumns,
);
const [createdRecord] = insertResult.generatedMaps;
this.logger.log(`Record created successfully in ${objectName}`);
return {
success: true,
message: `Record created successfully in ${objectName}`,
result: createdRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: createdRecord.id,
displayName: getRecordDisplayName(
{ ...transformedObjectRecord, ...createdRecord },
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: validObjectRecord,
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
});
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const insertResult = await repository.insert(
{
...transformedObjectRecord,
position,
createdBy: params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
},
},
undefined,
selectedColumns,
);
const [createdRecord] = insertResult.generatedMaps;
this.logger.log(`Record created successfully in ${objectName}`);
return {
success: true,
message: `Record created successfully in ${objectName}`,
result: createdRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: createdRecord.id,
displayName: getRecordDisplayName(
{ ...transformedObjectRecord, ...createdRecord },
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
},
);
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -10,14 +10,15 @@ import {
import { type DeleteRecordParams } from 'src/engine/core-modules/record-crud/types/delete-record-params.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
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';
@Injectable()
export class DeleteRecordService {
private readonly logger = new Logger(DeleteRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async execute(params: DeleteRecordParams): Promise<ToolOutput> {
@@ -45,85 +46,93 @@ export class DeleteRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const { flatObjectMetadataMaps, objectIdByNameSingular } =
repository.internalContext;
const { flatObjectMetadataMaps, objectIdByNameSingular } =
repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to delete: Object cannot be deleted by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to delete: Object cannot be deleted by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecord = await repository.findOne({
where: {
id: objectRecordId,
const objectRecord = await repository.findOne({
where: {
id: objectRecordId,
},
});
if (!objectRecord) {
throw new RecordCrudException(
`Failed to delete: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
if (soft) {
const columnsToReturnForSoftDelete: string[] = [];
await repository.softDelete(
objectRecordId,
undefined,
columnsToReturnForSoftDelete,
);
this.logger.log(
`Record soft deleted successfully from ${objectName}`,
);
return {
success: true,
message: `Record soft deleted successfully from ${objectName}`,
result: objectRecord,
};
} else {
await repository.remove(objectRecord);
this.logger.log(
`Record permanently deleted successfully from ${objectName}`,
);
return {
success: true,
message: `Record permanently deleted successfully from ${objectName}`,
result: { id: objectRecordId },
};
}
},
});
if (!objectRecord) {
throw new RecordCrudException(
`Failed to delete: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
if (soft) {
const columnsToReturnForSoftDelete: string[] = [];
await repository.softDelete(
objectRecordId,
undefined,
columnsToReturnForSoftDelete,
);
this.logger.log(`Record soft deleted successfully from ${objectName}`);
return {
success: true,
message: `Record soft deleted successfully from ${objectName}`,
result: objectRecord,
};
} else {
await repository.remove(objectRecord);
this.logger.log(
`Record permanently deleted successfully from ${objectName}`,
);
return {
success: true,
message: `Record permanently deleted successfully from ${objectName}`,
result: { id: objectRecordId },
};
}
);
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -25,16 +25,17 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
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';
@Injectable()
export class FindRecordsService {
private readonly logger = new Logger(FindRecordsService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async execute(
@@ -58,82 +59,88 @@ export class FindRecordsService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const graphqlQueryParser = new GraphqlQueryParser(
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const graphqlQueryParser = new GraphqlQueryParser(
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const records = await this.getObjectRecords({
objectName,
filter,
orderBy,
limit,
offset,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const records = await this.getObjectRecords({
objectName,
filter,
orderBy,
limit,
offset,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const totalCount = await this.getTotalCount({
objectName,
filter,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const totalCount = await this.getTotalCount({
objectName,
filter,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
this.logger.log(`Found ${records.length} records in ${objectName}`);
this.logger.log(`Found ${records.length} records in ${objectName}`);
const recordReferences = records.map((record) => ({
objectNameSingular: objectName,
recordId: record.id as string,
displayName: getRecordDisplayName(
record,
flatObjectMetadata,
flatFieldMetadataMaps,
),
}));
const recordReferences = records.map((record) => ({
objectNameSingular: objectName,
recordId: record.id as string,
displayName: getRecordDisplayName(
record,
flatObjectMetadata,
flatFieldMetadataMaps,
),
}));
return {
success: true,
message: `Found ${records.length} ${objectName} records`,
result: {
records,
count: totalCount,
return {
success: true,
message: `Found ${records.length} ${objectName} records`,
result: {
records,
count: totalCount,
},
recordReferences,
};
},
recordReferences,
};
);
} catch (error) {
this.logger.error(`Failed to find records: ${error}`);
@@ -14,14 +14,15 @@ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
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';
@Injectable()
export class UpdateRecordService {
private readonly logger = new Logger(UpdateRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
@@ -51,135 +52,141 @@ export class UpdateRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const previousObjectRecord = await repository.findOne({
where: {
id: objectRecordId,
},
select: selectedColumns,
});
const previousObjectRecord = await repository.findOne({
where: {
id: objectRecordId,
},
select: selectedColumns,
});
if (!previousObjectRecord) {
throw new RecordCrudException(
`Failed to update: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
if (!previousObjectRecord) {
throw new RecordCrudException(
`Failed to update: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
const fieldsToUpdateArray = fieldsToUpdate || Object.keys(objectRecord);
const fieldsToUpdateArray =
fieldsToUpdate || Object.keys(objectRecord);
if (fieldsToUpdateArray.length === 0) {
return {
success: true,
message: 'No fields to update',
result: previousObjectRecord,
};
}
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce(
(acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
if (fieldsToUpdateArray.length === 0) {
return {
...acc,
[key]: objectRecord[key],
success: true,
message: 'No fields to update',
result: previousObjectRecord,
};
}
return acc;
},
{},
);
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const objectRecordWithFilteredFields = Object.keys(
objectRecord,
).reduce((acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
return {
...acc,
[key]: objectRecord[key],
};
}
const updatedObjectRecord = {
...previousObjectRecord,
...objectRecordWithFilteredFields,
};
return acc;
}, {});
if (!deepEqual(updatedObjectRecord, previousObjectRecord)) {
await repository.update(
objectRecordId,
{
...transformedObjectRecord,
},
undefined,
selectedColumns,
);
}
this.logger.log(`Record updated successfully in ${objectName}`);
return {
success: true,
message: `Record updated successfully in ${objectName}`,
result: updatedObjectRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: objectRecordId,
displayName: getRecordDisplayName(
updatedObjectRecord,
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
});
const updatedObjectRecord = {
...previousObjectRecord,
...objectRecordWithFilteredFields,
};
if (!deepEqual(updatedObjectRecord, previousObjectRecord)) {
await repository.update(
objectRecordId,
{
...transformedObjectRecord,
},
undefined,
selectedColumns,
);
}
this.logger.log(`Record updated successfully in ${objectName}`);
return {
success: true,
message: `Record updated successfully in ${objectName}`,
result: updatedObjectRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: objectRecordId,
displayName: getRecordDisplayName(
updatedObjectRecord,
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
},
);
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -16,14 +16,15 @@ import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metad
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
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';
@Injectable()
export class UpsertRecordService {
private readonly logger = new Logger(UpsertRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
@@ -39,163 +40,167 @@ export class UpsertRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const fieldsToUpdateArray = Object.keys(objectRecord).filter((field) =>
isDefined(objectRecord[field]),
);
const fieldsToUpdateArray = Object.keys(objectRecord).filter(
(field) => isDefined(objectRecord[field]),
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce(
(acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
return {
...acc,
[key]: objectRecord[key],
};
}
return acc;
},
{},
);
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const uniqueFieldsToUpdate = fieldsToUpdateArray
.map(
(fieldName) =>
fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName],
)
.map((fieldId) => flatFieldMetadataMaps.byId[fieldId])
.filter(isDefined)
.filter((field) => field.isUnique || field.name === 'id');
const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap(
(field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = getCompositeTypeOrThrow(field.type);
const uniqueProperties = compositeType.properties.filter(
(prop) => prop.isIncludedInUniqueConstraint,
);
const propertiesToUse =
uniqueProperties.length > 0
? uniqueProperties
: [compositeType.properties[0]];
return propertiesToUse.map((prop) =>
computeCompositeColumnName(field, prop),
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
return [field.name];
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(
objectRecord,
).reduce((acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
return {
...acc,
[key]: objectRecord[key],
};
}
return acc;
}, {});
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const uniqueFieldsToUpdate = fieldsToUpdateArray
.map(
(fieldName) =>
fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName],
)
.map((fieldId) => flatFieldMetadataMaps.byId[fieldId])
.filter(isDefined)
.filter((field) => field.isUnique || field.name === 'id');
const conflictPathsUniqueFieldsToUpdate =
uniqueFieldsToUpdate.flatMap((field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = getCompositeTypeOrThrow(field.type);
const uniqueProperties = compositeType.properties.filter(
(prop) => prop.isIncludedInUniqueConstraint,
);
const propertiesToUse =
uniqueProperties.length > 0
? uniqueProperties
: [compositeType.properties[0]];
return propertiesToUse.map((prop) =>
computeCompositeColumnName(field, prop),
);
}
return [field.name];
});
const conflictPaths =
conflictPathsUniqueFieldsToUpdate.length > 0
? conflictPathsUniqueFieldsToUpdate
: ['id'];
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const upsertResult = await repository.upsert(
transformedObjectRecord,
{
conflictPaths: conflictPaths,
},
undefined,
selectedColumns,
);
const upsertedRecordId = upsertResult.identifiers?.[0].id;
if (!isDefined(upsertedRecordId)) {
throw new RecordCrudException(
`Failed to upsert record in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
const upsertedRecord = await repository.findOne({
where: {
id: upsertedRecordId,
},
select: selectedColumns,
});
if (!upsertedRecord) {
throw new RecordCrudException(
`Record not found after upsert with id ${upsertedRecordId} in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
this.logger.log(`Record upserted successfully in ${objectName}`);
return {
success: true,
message: `Record upserted successfully in ${objectName}`,
result: upsertedRecord,
};
},
);
const conflictPaths =
conflictPathsUniqueFieldsToUpdate.length > 0
? conflictPathsUniqueFieldsToUpdate
: ['id'];
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const upsertResult = await repository.upsert(
transformedObjectRecord,
{
conflictPaths: conflictPaths,
},
undefined,
selectedColumns,
);
const upsertedRecordId = upsertResult.identifiers?.[0].id;
if (!isDefined(upsertedRecordId)) {
throw new RecordCrudException(
`Failed to upsert record in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
const upsertedRecord = await repository.findOne({
where: {
id: upsertedRecordId,
},
select: selectedColumns,
});
if (!upsertedRecord) {
throw new RecordCrudException(
`Record not found after upsert with id ${upsertedRecordId} in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
this.logger.log(`Record upserted successfully in ${objectName}`);
return {
success: true,
message: `Record upserted successfully in ${objectName}`,
result: upsertedRecord,
};
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -1,10 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
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';
describe('RecordPositionService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let mockRepository: any;
let service: RecordPositionService;
@@ -16,16 +16,19 @@ describe('RecordPositionService', () => {
maximum: jest.fn().mockResolvedValue(1),
};
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
globalWorkspaceOrmManager = {
getRepository: jest.fn().mockResolvedValue(mockRepository),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
} as unknown as jest.Mocked<GlobalWorkspaceOrmManager>;
const module: TestingModule = await Test.createTestingModule({
providers: [
RecordPositionService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
],
}).compile();
@@ -4,7 +4,8 @@ import { isNumber } from '@sniptt/guards';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
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';
export type RecordPositionServiceCreateArgs = {
value: number | 'first' | 'last';
@@ -16,7 +17,7 @@ export type RecordPositionServiceCreateArgs = {
@Injectable()
export class RecordPositionService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async buildRecordPosition({
@@ -153,20 +154,26 @@ export class RecordPositionService {
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<{ id: string; position: number } | null> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
const record = await repository.findOneBy({
position: positionValue,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
return record ? { id: record.id, position: record.position } : null;
const record = await repository.findOneBy({
position: positionValue,
});
return record ? { id: record.id, position: record.position } : null;
},
);
}
async updatePosition(
@@ -175,49 +182,67 @@ export class RecordPositionService {
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<void> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
await repository.update(recordId, {
position: positionValue,
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
await repository.update(recordId, {
position: positionValue,
});
},
);
}
private async findMinPosition(
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<number | null> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
return await repository.minimum('position');
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
return await repository.minimum('position');
},
);
}
private async findMaxPosition(
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<number | null> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
return await repository.maximum('position');
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
return await repository.maximum('position');
},
);
}
}
@@ -7,7 +7,7 @@ import {
} from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { SearchService } from 'src/engine/core-modules/search/services/search.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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('SearchService', () => {
@@ -17,7 +17,7 @@ describe('SearchService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SearchService,
{ provide: TwentyORMGlobalManager, useValue: {} },
{ provide: GlobalWorkspaceOrmManager, useValue: {} },
{ provide: WorkspaceCacheStorageService, useValue: {} },
{ provide: FileService, useValue: {} },
],
@@ -30,9 +30,10 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
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 { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
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 RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
type LastRanks = { tsRankCD: number; tsRank: number };
@@ -46,7 +47,7 @@ const OBJECT_METADATA_ITEMS_CHUNK_SIZE = 5;
@Injectable()
export class SearchService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly fileService: FileService,
) {}
@@ -81,29 +82,36 @@ export class SearchService {
OBJECT_METADATA_ITEMS_CHUNK_SIZE,
);
const authContext = buildSystemAuthContext(workspaceId);
for (const objectMetadataItemChunk of filteredObjectMetadataItemsChunks) {
const recordsWithObjectMetadataItems = await Promise.all(
objectMetadataItemChunk.map(async (flatObjectMetadata) => {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ObjectRecord>(
workspaceId,
flatObjectMetadata.nameSingular,
rolePermissionConfig,
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository =
await this.globalWorkspaceOrmManager.getRepository<ObjectRecord>(
workspaceId,
flatObjectMetadata.nameSingular,
rolePermissionConfig,
);
return {
objectMetadataItem: flatObjectMetadata,
records: await this.buildSearchQueryAndGetRecords({
entityManager: repository,
flatObjectMetadata,
flatFieldMetadataMaps,
searchTerms: formatSearchTerms(searchInput, 'and'),
searchTermsOr: formatSearchTerms(searchInput, 'or'),
limit: limit as number,
filter: filter ?? ({} as ObjectRecordFilter),
after,
}),
};
return {
objectMetadataItem: flatObjectMetadata,
records: await this.buildSearchQueryAndGetRecords({
entityManager: repository,
flatObjectMetadata,
flatFieldMetadataMaps,
searchTerms: formatSearchTerms(searchInput, 'and'),
searchTermsOr: formatSearchTerms(searchInput, 'or'),
limit: limit as number,
filter: filter ?? ({} as ObjectRecordFilter),
after,
}),
};
},
);
}),
);
@@ -9,6 +9,7 @@ import { In, Repository } from 'typeorm';
import { z } from 'zod';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils';
import {
SendEmailToolException,
@@ -18,13 +19,13 @@ import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/t
import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
import { parseEmailBody } from 'src/utils/parse-email-body';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
@Injectable()
export class SendEmailTool implements Tool {
@@ -35,7 +36,7 @@ export class SendEmailTool implements Tool {
inputSchema = SendEmailToolParametersZodSchema;
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly sendMessageService: MessagingSendMessageService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
@@ -53,49 +54,63 @@ export class SendEmailTool implements Tool {
);
}
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
relations: {
messageChannels: {
messageFolders: true,
},
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
relations: {
messageChannels: {
messageFolders: true,
},
},
});
if (!isDefined(connectedAccount)) {
throw new SendEmailToolException(
`Connected Account '${connectedAccountId}' not found`,
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return connectedAccount;
},
});
if (!isDefined(connectedAccount)) {
throw new SendEmailToolException(
`Connected Account '${connectedAccountId}' not found`,
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return connectedAccount;
);
}
private async getOrThrowFirstConnectedAccountId(
workspaceId: string,
): Promise<string> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const allAccounts = await connectedAccountRepository.find();
const authContext = buildSystemAuthContext(workspaceId);
if (!allAccounts || allAccounts.length === 0) {
throw new SendEmailToolException(
'No connected accounts found for this workspace',
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const allAccounts = await connectedAccountRepository.find();
return allAccounts[0].id;
if (!allAccounts || allAccounts.length === 0) {
throw new SendEmailToolException(
'No connected accounts found for this workspace',
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return allAccounts[0].id;
},
);
}
private async getAttachments(
@@ -27,7 +27,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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 WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
describe('UserWorkspaceService', () => {
let service: UserWorkspaceService;
@@ -35,7 +36,7 @@ describe('UserWorkspaceService', () => {
let userRepository: Repository<UserEntity>;
let workspaceInvitationService: WorkspaceInvitationService;
let approvedAccessDomainService: ApprovedAccessDomainService;
let twentyORMGlobalManager: TwentyORMGlobalManager;
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let userRoleService: UserRoleService;
let fileService: FileService;
let fileUploadService: FileUploadService;
@@ -102,9 +103,12 @@ describe('UserWorkspaceService', () => {
},
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation(async (_authContext, callback) => callback()),
getRepository: jest.fn(),
},
},
{
@@ -156,9 +160,14 @@ describe('UserWorkspaceService', () => {
approvedAccessDomainService = module.get<ApprovedAccessDomainService>(
ApprovedAccessDomainService,
);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
(globalWorkspaceOrmManager.getRepository as jest.Mock).mockResolvedValue({
findOne: jest.fn(),
update: jest.fn(),
} as unknown as WorkspaceRepository<UserWorkspaceEntity>);
userRoleService = module.get<UserRoleService>(UserRoleService);
fileUploadService = module.get<FileUploadService>(FileUploadService);
onboardingService = module.get<OnboardingService>(OnboardingService);
@@ -374,7 +383,7 @@ describe('UserWorkspaceService', () => {
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(workspaceMember);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
jest.spyOn(userWorkspaceRepository, 'findOneOrFail').mockResolvedValue({
@@ -902,7 +911,7 @@ describe('UserWorkspaceService', () => {
};
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
const result = await service.getWorkspaceMemberOrThrow({
@@ -910,11 +919,13 @@ describe('UserWorkspaceService', () => {
workspaceId,
});
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith(workspaceId, 'workspaceMember', {
shouldBypassPermissionChecks: true,
});
expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith(
workspaceId,
'workspaceMember',
{
shouldBypassPermissionChecks: true,
},
);
expect(workspaceMemberRepository.findOne).toHaveBeenCalledWith({
where: {
id: workspaceMemberId,
@@ -931,7 +942,7 @@ describe('UserWorkspaceService', () => {
};
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
await expect(
@@ -33,7 +33,8 @@ import {
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
@@ -50,7 +51,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly loginTokenService: LoginTokenService,
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly userRoleService: UserRoleService,
private readonly fileUploadService: FileUploadService,
private readonly fileService: FileService,
@@ -92,41 +93,48 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
}
async createWorkspaceMember(workspaceId: string, user: UserEntity) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
where: {
userId: user.id,
workspaceId,
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
where: {
userId: user.id,
workspaceId,
},
});
await workspaceMemberRepository.insert({
name: {
firstName: user.firstName,
lastName: user.lastName,
},
colorScheme: 'System',
userId: user.id,
userEmail: user.email,
avatarUrl: userWorkspace.defaultAvatarUrl ?? '',
locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES,
});
const workspaceMember = await workspaceMemberRepository.find({
where: {
userId: user.id,
},
});
assert(
workspaceMember?.length === 1,
`Error while creating workspace member ${user.email} on workspace ${workspaceId}`,
);
},
});
await workspaceMemberRepository.insert({
name: {
firstName: user.firstName,
lastName: user.lastName,
},
colorScheme: 'System',
userId: user.id,
userEmail: user.email,
avatarUrl: userWorkspace.defaultAvatarUrl ?? '',
locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES,
});
const workspaceMember = await workspaceMemberRepository.find({
where: {
userId: user.id,
},
});
assert(
workspaceMember?.length === 1,
`Error while creating workspace member ${user.email} on workspace ${workspaceId}`,
);
}
@@ -343,24 +351,31 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
workspaceMemberId: string;
workspaceId: string;
}): Promise<WorkspaceMemberWorkspaceEntity> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
id: workspaceMemberId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
id: workspaceMemberId,
},
});
if (!isDefined(workspaceMember)) {
throw new Error('Workspace member not found');
}
return workspaceMember;
},
});
if (!isDefined(workspaceMember)) {
throw new Error('Workspace member not found');
}
return workspaceMember;
);
}
private async computeDefaultAvatarUrl(
@@ -4,7 +4,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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type UpdateWorkspaceMemberEmailJobData = {
@@ -21,7 +22,7 @@ export class UpdateWorkspaceMemberEmailJob {
constructor(
private readonly userWorkspaceService: UserWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
@Process(UpdateWorkspaceMemberEmailJob.name)
@@ -32,13 +33,23 @@ export class UpdateWorkspaceMemberEmailJob {
const workspace =
await this.userWorkspaceService.findFirstWorkspaceByUserId(userId);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
await workspaceMemberRepository.update({ userId }, { userEmail: email });
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
await workspaceMemberRepository.update(
{ userId },
{ userEmail: email },
);
},
);
}
}
@@ -20,15 +20,15 @@ import {
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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 { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
describe('UserService', () => {
let service: UserService;
let userRepository: Repository<UserEntity>;
let workspaceService: WorkspaceService;
let twentyORMGlobalManager: TwentyORMGlobalManager;
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let userRoleService: UserRoleService;
const mockWorkspaceMemberRepo = {
@@ -70,9 +70,13 @@ describe('UserService', () => {
useValue: { add: jest.fn() },
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
getRepository: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -99,8 +103,8 @@ describe('UserService', () => {
getRepositoryToken(UserEntity),
);
userRoleService = module.get<UserRoleService>(UserRoleService);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
workspaceService = module.get<WorkspaceService>(WorkspaceService);
});
@@ -115,9 +119,7 @@ describe('UserService', () => {
);
expect(res).toBeNull();
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).not.toHaveBeenCalled();
expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled();
});
it('fetches from workspace member repo when workspace active', async () => {
@@ -127,7 +129,7 @@ describe('UserService', () => {
} as WorkspaceMemberWorkspaceEntity);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
const res = await service.loadWorkspaceMember(
@@ -138,11 +140,13 @@ describe('UserService', () => {
} as WorkspaceEntity,
);
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith('w1', 'workspaceMember', {
shouldBypassPermissionChecks: true,
});
expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith(
'w1',
'workspaceMember',
{
shouldBypassPermissionChecks: true,
},
);
expect(mockWorkspaceMemberRepo.findOne).toHaveBeenCalledWith({
where: { userId: 'u1' },
});
@@ -158,9 +162,7 @@ describe('UserService', () => {
} as WorkspaceEntity);
expect(res).toEqual([]);
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).not.toHaveBeenCalled();
expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled();
});
it('fetches members withDeleted flag', async () => {
@@ -168,7 +170,7 @@ describe('UserService', () => {
.spyOn(mockWorkspaceMemberRepo, 'find')
.mockResolvedValue([{ id: 'wm1' } as WorkspaceMemberWorkspaceEntity]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
const res = await service.loadWorkspaceMembers(
@@ -203,7 +205,7 @@ describe('UserService', () => {
{ id: 'wm-del' } as WorkspaceMemberWorkspaceEntity,
]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
await service.loadDeletedWorkspaceMembersOnly({
@@ -323,7 +325,7 @@ describe('UserService', () => {
{ id: 'wm-2', userId: 'uX' } as WorkspaceMemberWorkspaceEntity,
]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
jest
@@ -358,7 +360,7 @@ describe('UserService', () => {
.spyOn(mockWorkspaceMemberRepo, 'find')
.mockResolvedValue([wmForUser('u2')]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
(userRepository.softDelete as jest.Mock).mockResolvedValue({
@@ -37,7 +37,8 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@@ -48,7 +49,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly emailVerificationService: EmailVerificationService,
private readonly workspaceService: WorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly userRoleService: UserRoleService,
private readonly userWorkspaceService: UserWorkspaceService,
@InjectMessageQueue(MessageQueue.workspaceQueue)
@@ -62,18 +63,25 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
return null;
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
return await workspaceMemberRepository.findOne({
where: {
userId: user.id,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
},
});
);
}
async loadWorkspaceMembers(workspace: WorkspaceEntity, withDeleted = false) {
@@ -81,14 +89,23 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
return [];
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
return await workspaceMemberRepository.find({ withDeleted: withDeleted });
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.find({
withDeleted: withDeleted,
});
},
);
}
async loadDeletedWorkspaceMembersOnly(workspace: WorkspaceEntity) {
@@ -96,17 +113,24 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
return [];
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
return await workspaceMemberRepository.find({
where: { deletedAt: Not(IsNull()) },
withDeleted: true,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.find({
where: { deletedAt: Not(IsNull()) },
withDeleted: true,
});
},
);
}
async deleteUser(userId: string) {
@@ -174,16 +198,23 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
userWorkspace: UserWorkspaceEntity,
) {
const workspaceId = userWorkspace.workspaceId;
const authContext = buildSystemAuthContext(workspaceId);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
const workspaceMembers =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.find();
},
);
const workspaceMembers = await workspaceMemberRepository.find();
const userWorkspaceId = userWorkspace.id;
if (workspaceMembers.length === 1) {
@@ -224,7 +255,21 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
assert(workspaceMember, 'WorkspaceMember not found');
await workspaceMemberRepository.delete({ userId: userWorkspace.userId });
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
await workspaceMemberRepository.delete({
userId: userWorkspace.userId,
});
},
);
await this.userWorkspaceService.deleteUserWorkspace({
userWorkspaceId,
@@ -14,10 +14,10 @@ import crypto from 'crypto';
import { msg } from '@lingui/core/macro';
import { GraphQLJSONObject } from 'graphql-type-json';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
@@ -73,7 +73,8 @@ import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permi
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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 { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -103,7 +104,7 @@ export class UserResolver {
private readonly permissionsService: PermissionsService,
private readonly workspaceMemberTranspiler: WorkspaceMemberTranspiler,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
private async getUserWorkspacePermissions({
@@ -425,18 +426,26 @@ export class UserResolver {
);
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMemberToDelete = await workspaceMemberRepository.findOne({
where: {
id: workspaceMemberIdToDelete,
},
});
const workspaceMemberToDelete =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOne({
where: {
id: workspaceMemberIdToDelete,
},
});
},
);
if (!isDefined(workspaceMemberToDelete)) {
throw new BadRequestException(
@@ -9,14 +9,15 @@ import {
} from '@nestjs/common';
import { Request } from 'express';
import { isDefined } from 'twenty-shared/utils';
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-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,
@@ -36,7 +37,7 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t
)
export class WorkflowTriggerController {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
) {}
@@ -72,68 +73,78 @@ export class WorkflowTriggerController {
payload?: object;
workspaceId: string;
}) {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflow = await workflowRepository.findOne({
where: { id: workflowId },
});
const { workflow } =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
if (!isDefined(workflow)) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
const workflow = await workflowRepository.findOne({
where: { id: workflowId },
});
if (
!isDefined(workflow.lastPublishedVersionId) ||
workflow.lastPublishedVersionId === ''
) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
);
}
if (!isDefined(workflow)) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: { id: workflow.lastPublishedVersionId },
});
if (
!isDefined(workflow.lastPublishedVersionId) ||
workflow.lastPublishedVersionId === ''
) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
);
}
if (!isDefined(workflowVersion)) {
throw new WorkflowTriggerException(
`[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: { id: workflow.lastPublishedVersionId },
});
if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (!isDefined(workflowVersion)) {
throw new WorkflowTriggerException(
`[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
);
}
return { workflow, workflowVersion };
},
);
}
const { workflowRunId } =
await this.workflowTriggerWorkspaceService.runWorkflowVersion({
workflowVersionId: workflow.lastPublishedVersionId,
workflowVersionId: workflow.lastPublishedVersionId!,
payload: payload || {},
createdBy: {
source: FieldActorSource.WEBHOOK,
@@ -19,7 +19,8 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-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 { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -37,7 +38,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
)
export class WorkflowTriggerResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
) {}
@@ -72,18 +73,26 @@ export class WorkflowTriggerResolver {
@Args('input')
{ workflowVersionId, workflowRunId, payload }: RunWorkflowVersionInput,
) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
},
);
return this.workflowTriggerWorkspaceService.runWorkflowVersion({
workflowVersionId,
@@ -16,7 +16,8 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { SendInvitationsInput } from './dtos/send-invitations.input';
@@ -33,7 +34,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input';
@Resolver()
export class WorkspaceInvitationResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceInvitationService: WorkspaceInvitationService,
) {}
@@ -55,18 +56,26 @@ export class WorkspaceInvitationResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUser() user: UserEntity,
) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
},
);
return this.workspaceInvitationService.resendWorkspaceInvitation(
appTokenId,
@@ -87,18 +96,26 @@ export class WorkspaceInvitationResolver {
@AuthUser() user: UserEntity,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<SendInvitationsOutput> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
},
);
return await this.workspaceInvitationService.sendInvitations(
sendInviteLinkInput.emails,
@@ -9,7 +9,8 @@ import {
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.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';
export type AgentActorContext = {
actorContext: ActorMetadata;
@@ -22,13 +23,15 @@ export class AgentActorContextService {
constructor(
private readonly userWorkspaceService: UserWorkspaceService,
private readonly userRoleService: UserRoleService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async buildUserAndAgentActorContext(
userWorkspaceId: string,
workspaceId: string,
): Promise<AgentActorContext> {
const authContext = buildSystemAuthContext(workspaceId);
const userWorkspace =
await this.userWorkspaceService.findById(userWorkspaceId);
@@ -39,18 +42,24 @@ export class AgentActorContextService {
);
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: userWorkspace.userId,
},
});
return workspaceMemberRepository.findOne({
where: {
userId: userWorkspace.userId,
},
});
},
);
if (!workspaceMember) {
throw new AgentException(
@@ -44,8 +44,9 @@ import { CHAT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-chat/cons
import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
export type ChatExecutionOptions = {
workspace: WorkspaceEntity;
@@ -75,8 +76,7 @@ export class ChatExecutionService {
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly aiBillingService: AIBillingService,
private readonly agentActorContextService: AgentActorContextService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
@@ -212,106 +212,116 @@ export class ChatExecutionService {
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType,
userWorkspaceId: string,
): Promise<string> {
const { userWorkspaceRoleMap } =
await this.workspaceCacheService.getOrRecompute(workspace.id, [
'userWorkspaceRoleMap',
]);
const authContext = buildSystemAuthContext(workspace.id);
const roleId = userWorkspaceRoleMap[userWorkspaceId];
const contextFromRecords =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
permissionsPerRoleId: objectMetadataPermissions,
userWorkspaceRoleMap,
} = getWorkspaceContext();
if (!roleId) {
throw new AgentException(
'Failed to retrieve user role.',
AgentExceptionCode.ROLE_NOT_FOUND,
);
}
const roleId = userWorkspaceRoleMap[userWorkspaceId];
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
});
if (!roleId) {
throw new AgentException(
'Failed to retrieve user role.',
AgentExceptionCode.ROLE_NOT_FOUND,
);
}
const flatObjectMetadataMaps =
workspaceDataSource.internalContext.flatObjectMetadataMaps;
const flatFieldMetadataMaps =
workspaceDataSource.internalContext.flatFieldMetadataMaps;
const objectIdByNameSingular =
workspaceDataSource.internalContext.objectIdByNameSingular;
const objectMetadataPermissions = workspaceDataSource.permissionsPerRoleId;
const contextObject = (
await Promise.all(
recordIdsByObjectMetadataNameSingular.map(
async (recordsWithObjectMetadataNameSingular) => {
if (recordsWithObjectMetadataNameSingular.recordIds.length === 0) {
return [];
}
const objectMetadataId =
objectIdByNameSingular[
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular
];
const objectMetadataMapItem = objectMetadataId
? flatObjectMetadataMaps.byId[objectMetadataId]
: undefined;
if (!objectMetadataMapItem) {
this.logger.warn(
`Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`,
);
return [];
}
const repository = workspaceDataSource.getRepository(
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
{ unionOf: [roleId] },
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getDataSourceForWorkspace(
workspace.id,
);
const restrictedFields =
objectMetadataPermissions?.[roleId]?.[objectMetadataMapItem.id]
?.restrictedFields ?? {};
const contextObject = (
await Promise.all(
recordIdsByObjectMetadataNameSingular.map(
async (recordsWithObjectMetadataNameSingular) => {
if (
recordsWithObjectMetadataNameSingular.recordIds.length === 0
) {
return [];
}
const hasRestrictedFields = Object.values(restrictedFields).some(
(field) => field.canRead === false,
);
const objectMetadataId =
objectIdByNameSingular[
recordsWithObjectMetadataNameSingular
.objectMetadataNameSingular
];
const objectMetadataMapItem = objectMetadataId
? flatObjectMetadataMaps.byId[objectMetadataId]
: undefined;
const selectOptions = hasRestrictedFields
? getAllSelectableColumnNames({
restrictedFields,
objectMetadata: {
objectMetadataMapItem,
flatFieldMetadataMaps,
},
})
: undefined;
if (!objectMetadataMapItem) {
this.logger.warn(
`Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`,
);
return (
await repository.find({
...(selectOptions && { select: selectOptions }),
where: {
id: In(recordsWithObjectMetadataNameSingular.recordIds),
return [];
}
const repository = workspaceDataSource.getRepository(
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
{ unionOf: [roleId] },
);
const restrictedFields =
objectMetadataPermissions?.[roleId]?.[
objectMetadataMapItem.id
]?.restrictedFields ?? {};
const hasRestrictedFields = Object.values(
restrictedFields,
).some((field) => field.canRead === false);
const selectOptions = hasRestrictedFields
? getAllSelectableColumnNames({
restrictedFields,
objectMetadata: {
objectMetadataMapItem,
flatFieldMetadataMaps,
},
})
: undefined;
return (
await repository.find({
...(selectOptions && { select: selectOptions }),
where: {
id: In(recordsWithObjectMetadataNameSingular.recordIds),
},
})
).map((record) => {
return {
...record,
resourceUrl:
this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.RecordShowPage, {
objectNameSingular:
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
objectRecordId: record.id,
}),
}),
};
});
},
})
).map((record) => {
return {
...record,
resourceUrl: this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.RecordShowPage, {
objectNameSingular:
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
objectRecordId: record.id,
}),
}),
};
});
},
),
)
).flat(2);
),
)
).flat(2);
return JSON.stringify(contextObject);
return JSON.stringify(contextObject);
},
);
return contextFromRecords;
}
private getLastUserMessage(
@@ -31,7 +31,8 @@ import {
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
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 { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
@@ -46,7 +47,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly applicationService: ApplicationService,
) {
super(objectMetadataRepository);
@@ -509,18 +510,25 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
view: FlatView;
workspaceId: string;
}) {
const favoriteRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<FavoriteWorkspaceEntity>(
workspaceId,
'favorite',
);
const authContext = buildSystemAuthContext(workspaceId);
const favoriteCount = await favoriteRepository.count();
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const favoriteRepository =
await this.globalWorkspaceOrmManager.getRepository<FavoriteWorkspaceEntity>(
workspaceId,
'favorite',
);
await favoriteRepository.insert({
viewId: view.id,
position: favoriteCount,
});
const favoriteCount = await favoriteRepository.count();
await favoriteRepository.insert({
viewId: view.id,
position: favoriteCount,
});
},
);
}
public async deleteWorkspaceAllObjectMetadata({
@@ -30,7 +30,8 @@ import {
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util';
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
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 { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@@ -39,7 +40,7 @@ export class PageLayoutService {
private readonly logger = new Logger(PageLayoutService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
@@ -376,23 +377,30 @@ export class PageLayoutService {
pageLayoutId: string,
workspaceId: string,
): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
try {
const dashboardRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
},
});
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
},
});
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
);
} catch (error) {
this.logger.error(
`Failed to destroy associated dashboards for page layout ${pageLayoutId}: ${error}`,
@@ -13,7 +13,8 @@ import {
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -24,7 +25,7 @@ export class UserRoleService {
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly roleTargetService: RoleTargetService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
@@ -137,6 +138,8 @@ export class UserRoleService {
roleId: string,
workspaceId: string,
): Promise<WorkspaceMemberWorkspaceEntity[]> {
const authContext = buildSystemAuthContext(workspaceId);
const userWorkspaceIdsWithRole =
await this.getUserWorkspaceIdsAssignedToRole(roleId, workspaceId);
@@ -150,20 +153,25 @@ export class UserRoleService {
userWorkspaces.map((userWorkspace) => userWorkspace.userId),
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMembers = await workspaceMemberRepository.find({
where: {
userId: In(userIds),
const workspaceMembers = await workspaceMemberRepository.find({
where: {
userId: In(userIds),
},
});
return workspaceMembers;
},
});
return workspaceMembers;
);
}
public async getUserWorkspaceIdsAssignedToRole(
@@ -8,7 +8,7 @@ import {
import { capitalize } from 'twenty-shared/utils';
import { metadataToRepositoryMapping } from 'src/engine/object-metadata-repository/metadata-to-repository.mapping';
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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
@@ -31,10 +31,10 @@ export class ObjectMetadataRepositoryModule {
provide: `${capitalize(
convertClassNameToObjectMetadataName(objectMetadata.name),
)}Repository`,
useFactory: (twentyORMGlobalManager: TwentyORMGlobalManager) => {
return new repositoryClass(twentyORMGlobalManager);
useFactory: (globalWorkspaceOrmManager: GlobalWorkspaceOrmManager) => {
return new repositoryClass(globalWorkspaceOrmManager);
},
inject: [TwentyORMGlobalManager],
inject: [GlobalWorkspaceOrmManager],
};
});
@@ -2,20 +2,23 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { TrashCleanupService } from 'src/engine/trash-cleanup/services/trash-cleanup.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';
describe('TrashCleanupService', () => {
let service: TrashCleanupService;
let mockFlatEntityMapsCacheService: any;
let mockTwentyORMGlobalManager: any;
let mockGlobalWorkspaceOrmManager: any;
beforeEach(async () => {
mockFlatEntityMapsCacheService = {
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
};
mockTwentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn(),
mockGlobalWorkspaceOrmManager = {
getRepository: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
};
const module: TestingModule = await Test.createTestingModule({
@@ -26,8 +29,8 @@ describe('TrashCleanupService', () => {
useValue: mockFlatEntityMapsCacheService,
},
{
provide: TwentyORMGlobalManager,
useValue: mockTwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: mockGlobalWorkspaceOrmManager,
},
],
}).compile();
@@ -99,7 +102,7 @@ describe('TrashCleanupService', () => {
const companyRepository = createRepositoryMock('company', 2);
const personRepository = createRepositoryMock('person', 1);
mockTwentyORMGlobalManager.getRepositoryForWorkspace
mockGlobalWorkspaceOrmManager.getRepository
.mockResolvedValueOnce(companyRepository)
.mockResolvedValueOnce(personRepository);
@@ -130,7 +133,7 @@ describe('TrashCleanupService', () => {
expect(result).toEqual(0);
expect(
mockTwentyORMGlobalManager.getRepositoryForWorkspace,
mockGlobalWorkspaceOrmManager.getRepository,
).not.toHaveBeenCalled();
});
@@ -145,7 +148,7 @@ describe('TrashCleanupService', () => {
const companyRepository = createRepositoryMock('company', 2);
const personRepository = createRepositoryMock('person', 5);
mockTwentyORMGlobalManager.getRepositoryForWorkspace
mockGlobalWorkspaceOrmManager.getRepository
.mockResolvedValueOnce(companyRepository)
.mockResolvedValueOnce(personRepository);
@@ -170,7 +173,7 @@ describe('TrashCleanupService', () => {
const companyRepository = createRepositoryMock('company', 0);
mockTwentyORMGlobalManager.getRepositoryForWorkspace.mockResolvedValueOnce(
mockGlobalWorkspaceOrmManager.getRepository.mockResolvedValueOnce(
companyRepository,
);
@@ -188,7 +191,7 @@ describe('TrashCleanupService', () => {
const companyRepository = createRepositoryMock('company', 5);
mockTwentyORMGlobalManager.getRepositoryForWorkspace.mockResolvedValueOnce(
mockGlobalWorkspaceOrmManager.getRepository.mockResolvedValueOnce(
companyRepository,
);
@@ -6,7 +6,8 @@ import { In, LessThan } from 'typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { TRASH_CLEANUP_BATCH_SIZE } from 'src/engine/trash-cleanup/constants/trash-cleanup-batch-size.constant';
import { TRASH_CLEANUP_MAX_RECORDS_PER_WORKSPACE } from 'src/engine/trash-cleanup/constants/trash-cleanup-max-records-per-workspace.constant';
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';
export type TrashCleanupInput = {
workspaceId: string;
@@ -22,7 +23,7 @@ export class TrashCleanupService {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async cleanupWorkspaceTrash(input: TrashCleanupInput): Promise<number> {
@@ -96,41 +97,47 @@ export class TrashCleanupService {
return 0;
}
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
let deleted = 0;
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
{ shouldBypassPermissionChecks: true },
);
while (deleted < remainingQuota) {
const take = Math.min(this.batchSize, remainingQuota - deleted);
let deleted = 0;
const recordsToDelete = await repository.find({
withDeleted: true,
select: ['id'],
where: {
deletedAt: LessThan(cutoffDate),
},
order: { deletedAt: 'ASC' },
take,
loadEagerRelations: false,
});
while (deleted < remainingQuota) {
const take = Math.min(this.batchSize, remainingQuota - deleted);
if (recordsToDelete.length === 0) {
break;
}
const recordsToDelete = await repository.find({
withDeleted: true,
select: ['id'],
where: {
deletedAt: LessThan(cutoffDate),
},
order: { deletedAt: 'ASC' },
take,
loadEagerRelations: false,
});
await repository.delete({
id: In(recordsToDelete.map((record) => record.id)),
});
if (recordsToDelete.length === 0) {
break;
}
deleted += recordsToDelete.length;
}
await repository.delete({
id: In(recordsToDelete.map((record) => record.id)),
});
return deleted;
deleted += recordsToDelete.length;
}
return deleted;
},
);
}
private calculateCutoffDate(trashRetentionDays: number): Date {
@@ -13,6 +13,7 @@ import {
import { EntityManagerFactory } from 'typeorm/entity-manager/EntityManagerFactory';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -29,7 +30,11 @@ type CreateQueryBuilderOptions = {
calledByWorkspaceEntityManager?: boolean;
};
export class WorkspaceDataSource extends DataSource {
export class WorkspaceDataSource
extends DataSource
implements WorkspaceDataSourceInterface
{
readonly isGlobalFlow = false;
readonly internalContext: WorkspaceInternalContext;
readonly manager: WorkspaceEntityManager;
featureFlagMapVersion: string;
@@ -34,6 +34,7 @@ import { type UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { InstanceChecker } from 'typeorm/util/InstanceChecker';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
@@ -57,12 +58,14 @@ import {
} from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
type PermissionOptions = {
shouldBypassPermissionChecks?: boolean;
@@ -70,19 +73,52 @@ type PermissionOptions = {
};
export class WorkspaceEntityManager extends EntityManager {
private readonly internalContext: WorkspaceInternalContext;
private readonly _legacyInternalContext?: WorkspaceInternalContext;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly repositories: Map<string, Repository<any>>;
declare connection: WorkspaceDataSource;
constructor(
internalContext: WorkspaceInternalContext,
legacyInternalContext: WorkspaceInternalContext | undefined,
connection: WorkspaceDataSource | GlobalWorkspaceDataSource,
queryRunner?: QueryRunner,
) {
super(connection, queryRunner);
this.internalContext = internalContext;
this.repositories = new Map();
this._legacyInternalContext = legacyInternalContext;
}
private get eventEmitterService(): WorkspaceEventEmitter {
const isGlobalFlow = (this.connection as WorkspaceDataSourceInterface)
.isGlobalFlow;
if (isGlobalFlow) {
return (this.connection as unknown as GlobalWorkspaceDataSource)
.eventEmitterService;
}
return this._legacyInternalContext!.eventEmitterService;
}
get internalContext(): WorkspaceInternalContext {
const isGlobalFlow = (this.connection as WorkspaceDataSourceInterface)
.isGlobalFlow;
if (isGlobalFlow) {
const context = getWorkspaceContext();
return {
workspaceId: context.authContext.workspace.id,
flatObjectMetadataMaps: context.flatObjectMetadataMaps,
flatFieldMetadataMaps: context.flatFieldMetadataMaps,
flatIndexMaps: context.flatIndexMaps,
objectIdByNameSingular: context.objectIdByNameSingular,
featureFlagsMap: context.featureFlagsMap,
eventEmitterService: this.eventEmitterService,
};
}
return this._legacyInternalContext!;
}
getFeatureFlagMap(): FeatureFlagMap {
@@ -146,7 +182,6 @@ export class WorkspaceEntityManager extends EntityManager {
}
const newRepository = new WorkspaceRepository<Entity>(
this.internalContext,
target,
this,
dataSource.featureFlagMap,
@@ -15,6 +15,7 @@ import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-sche
import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceORMEntityMetadatasCacheService } from 'src/engine/twenty-orm/global-workspace-datasource/workspace-orm-entity-metadatas-cache.service';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
@@ -35,6 +36,7 @@ import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/
TwentyConfigModule,
WorkspaceEventEmitterModule,
WorkspaceCacheModule,
TwentyORMModule,
],
providers: [
GlobalWorkspaceDataSourceService,
@@ -15,6 +15,7 @@ import { EntityMetadataNotFoundError } from 'typeorm/error/EntityMetadataNotFoun
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import {
PermissionsException,
@@ -31,8 +32,12 @@ type CreateQueryBuilderOptions = {
calledByWorkspaceEntityManager?: boolean;
};
export class GlobalWorkspaceDataSource extends DataSource {
private eventEmitterService: WorkspaceEventEmitter;
export class GlobalWorkspaceDataSource
extends DataSource
implements WorkspaceDataSourceInterface
{
readonly isGlobalFlow = true;
readonly eventEmitterService: WorkspaceEventEmitter;
private _isConstructing = true;
dataSourceWithOverridenCreateQueryBuilder: GlobalWorkspaceDataSource;
@@ -102,21 +107,7 @@ export class GlobalWorkspaceDataSource extends DataSource {
return super.createEntityManager(queryRunner) as WorkspaceEntityManager;
}
const context = getWorkspaceContext();
return new WorkspaceEntityManager(
{
workspaceId: context.authContext.workspace.id,
flatObjectMetadataMaps: context.flatObjectMetadataMaps,
flatFieldMetadataMaps: context.flatFieldMetadataMaps,
flatIndexMaps: context.flatIndexMaps,
objectIdByNameSingular: context.objectIdByNameSingular,
featureFlagsMap: context.featureFlagsMap,
eventEmitterService: this.eventEmitterService,
},
this,
queryRunner,
);
return new WorkspaceEntityManager(undefined, this, queryRunner);
}
override createQueryRunner(
@@ -1,17 +1,20 @@
import { Injectable, type Type } from '@nestjs/common';
import { ObjectLiteral } from 'typeorm';
import { type ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import type { WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import type { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import {
type ORMWorkspaceContext,
withWorkspaceContext,
} from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import type { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
@@ -20,8 +23,21 @@ export class GlobalWorkspaceOrmManager {
constructor(
private readonly globalWorkspaceDataSourceService: GlobalWorkspaceDataSourceService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async isGlobalDataSourceFlow(workspaceId: string): Promise<boolean> {
const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute(
workspaceId,
['featureFlagsMap'],
);
return (
featureFlagsMap[FeatureFlagKey.IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED] ===
true
);
}
async getRepository<T extends ObjectLiteral>(
workspaceId: string,
workspaceEntity: Type<T>,
@@ -39,25 +55,57 @@ export class GlobalWorkspaceOrmManager {
workspaceEntityOrObjectMetadataName: Type<T> | string,
permissionOptions?: RolePermissionConfig,
): Promise<WorkspaceRepository<T>> {
let objectMetadataName: string;
const isGlobalFlow = await this.isGlobalDataSourceFlow(workspaceId);
if (typeof workspaceEntityOrObjectMetadataName === 'string') {
objectMetadataName = workspaceEntityOrObjectMetadataName;
} else {
objectMetadataName = convertClassNameToObjectMetadataName(
workspaceEntityOrObjectMetadataName.name,
if (isGlobalFlow) {
let objectMetadataName: string;
if (typeof workspaceEntityOrObjectMetadataName === 'string') {
objectMetadataName = workspaceEntityOrObjectMetadataName;
} else {
objectMetadataName = convertClassNameToObjectMetadataName(
workspaceEntityOrObjectMetadataName.name,
);
}
const globalDataSource =
this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource();
return globalDataSource.getRepository<T>(
objectMetadataName,
permissionOptions,
);
}
const globalDataSource =
this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource();
if (typeof workspaceEntityOrObjectMetadataName === 'string') {
return this.twentyORMGlobalManager.getRepositoryForWorkspace<T>(
workspaceId,
workspaceEntityOrObjectMetadataName,
permissionOptions,
);
}
return globalDataSource.getRepository<T>(
objectMetadataName,
return this.twentyORMGlobalManager.getRepositoryForWorkspace<T>(
workspaceId,
workspaceEntityOrObjectMetadataName,
permissionOptions,
);
}
async getDataSourceForWorkspace(
workspaceId: string,
): Promise<WorkspaceDataSourceInterface> {
const isGlobalFlow = await this.isGlobalDataSourceFlow(workspaceId);
if (isGlobalFlow) {
return this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource();
}
return this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
}
async getGlobalWorkspaceDataSource() {
return this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource();
}
@@ -71,6 +119,18 @@ export class GlobalWorkspaceOrmManager {
return withWorkspaceContext(context, fn);
}
async destroyDataSourceForWorkspace(workspaceId: string): Promise<void> {
const isGlobalFlow = await this.isGlobalDataSourceFlow(workspaceId);
if (isGlobalFlow) {
return;
}
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
private async loadWorkspaceContext(
authContext: WorkspaceAuthContext,
): Promise<ORMWorkspaceContext> {
@@ -83,6 +143,7 @@ export class GlobalWorkspaceOrmManager {
featureFlagsMap,
rolesPermissions: permissionsPerRoleId,
ORMEntityMetadatas: entityMetadatas,
userWorkspaceRoleMap,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
@@ -90,6 +151,7 @@ export class GlobalWorkspaceOrmManager {
'featureFlagsMap',
'rolesPermissions',
'ORMEntityMetadatas',
'userWorkspaceRoleMap',
]);
const { idByNameSingular: objectIdByNameSingular } =
@@ -104,6 +166,7 @@ export class GlobalWorkspaceOrmManager {
featureFlagsMap,
permissionsPerRoleId,
entityMetadatas,
userWorkspaceRoleMap,
};
}
}
@@ -0,0 +1,49 @@
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import {
type EntityManager,
type EntityTarget,
type ObjectLiteral,
type QueryRunner,
type ReplicationMode,
} from 'typeorm';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export interface WorkspaceDataSourceInterface {
readonly isGlobalFlow: boolean;
readonly manager: EntityManager;
featureFlagMap: FeatureFlagMap;
permissionsPerRoleId: ObjectsPermissionsByRoleId;
getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
permissionOptions?: RolePermissionConfig,
authContext?: AuthContext,
): WorkspaceRepository<Entity>;
createQueryRunner(mode?: ReplicationMode): WorkspaceQueryRunner;
createEntityManager(queryRunner?: QueryRunner): WorkspaceEntityManager;
createQueryRunnerForEntityPersistExecutor(
mode?: ReplicationMode,
): QueryRunner;
transaction<T>(
runInTransaction: (entityManager: EntityManager) => Promise<T>,
): Promise<T>;
query<T = unknown>(
query: string,
parameters?: unknown[],
queryRunner?: QueryRunner,
options?: { shouldBypassPermissionChecks?: boolean },
): Promise<T>;
}
@@ -59,6 +59,9 @@ describe('WorkspaceRepository', () => {
decrement: jest.fn(),
preload: jest.fn(),
clear: jest.fn(),
get internalContext() {
return mockInternalContext;
},
} as unknown as jest.Mocked<WorkspaceEntityManager>;
const mockFieldMetadata: FlatFieldMetadata = {
@@ -136,7 +139,6 @@ describe('WorkspaceRepository', () => {
mockQueryRunner = {} as QueryRunner;
repository = new WorkspaceRepository(
mockInternalContext,
'test-entity',
mockEntityManager,
mockFeatureFlagMap,
@@ -37,7 +37,6 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g
export class WorkspaceRepository<
T extends ObjectLiteral,
> extends Repository<T> {
private readonly _internalContext: WorkspaceInternalContext;
private shouldBypassPermissionChecks: boolean;
private featureFlagMap: FeatureFlagMap;
public readonly objectRecordsPermissions?: ObjectsPermissions;
@@ -45,11 +44,10 @@ export class WorkspaceRepository<
declare manager: WorkspaceEntityManager;
get internalContext(): WorkspaceInternalContext {
return this._internalContext;
return this.manager.internalContext;
}
constructor(
internalContext: WorkspaceInternalContext,
target: EntityTarget<T>,
manager: WorkspaceEntityManager,
featureFlagMap: FeatureFlagMap,
@@ -59,7 +57,6 @@ export class WorkspaceRepository<
authContext?: AuthContext,
) {
super(target, manager, queryRunner);
this._internalContext = internalContext;
this.featureFlagMap = featureFlagMap;
this.objectRecordsPermissions = objectRecordsPermissions;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
@@ -83,7 +80,7 @@ export class WorkspaceRepository<
return new WorkspaceSelectQueryBuilder(
queryBuilder,
this.objectRecordsPermissions,
this._internalContext,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext ?? {},
this.featureFlagMap,
@@ -948,10 +945,7 @@ export class WorkspaceRepository<
* PRIVATE METHODS
*/
private async getObjectMetadataFromTarget() {
return getObjectMetadataFromEntityTarget(
this.target,
this._internalContext,
);
return getObjectMetadataFromEntityTarget(this.target, this.internalContext);
}
private async transformOptions<
@@ -978,7 +972,7 @@ export class WorkspaceRepository<
return formatData(
data,
objectMetadata,
this._internalContext.flatFieldMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
) as T;
}
}
@@ -20,6 +20,7 @@ export type ORMWorkspaceContext = {
featureFlagsMap: Record<FeatureFlagKey, boolean>;
permissionsPerRoleId: ObjectsPermissionsByRoleId;
entityMetadatas: EntityMetadata[];
userWorkspaceRoleMap: Record<string, string>;
};
export const workspaceContextStorage =

Some files were not shown because too many files have changed in this diff Show More