Files
upstream-twenty/packages/twenty-server/src/database/commands/command-runners/workspace-iterator.service.ts
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

222 lines
6.9 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { isNonEmptyString } from '@sniptt/guards';
import {
PROVISIONED_WORKSPACE_ACTIVATION_STATUSES,
WorkspaceActivationStatus,
} from 'twenty-shared/workspace';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, MoreThanOrEqual, Raw, Repository } from 'typeorm';
import { CommandShutdownService } from 'src/database/commands/command-runners/command-shutdown.service';
import { activationStatusIn } from 'src/database/commands/command-runners/utils/activation-status-in.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceOrmManager } from 'src/engine/twenty-orm/workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
export type WorkspaceIteratorShard = {
index: number;
total: number;
};
export type WorkspaceIteratorArgs = {
workspaceIds?: string[];
activationStatuses?: WorkspaceActivationStatus[];
startFromWorkspaceId?: string;
workspaceCountLimit?: number;
shard?: WorkspaceIteratorShard;
dryRun?: boolean;
callback: (context: WorkspaceIteratorContext) => Promise<void>;
};
export type WorkspaceIteratorContext = {
workspaceId: string;
databaseSchema?: string;
dataSource?: DataSource;
index: number;
total: number;
};
export type WorkspaceIteratorReport = {
fail: {
workspaceId: string;
error: Error;
}[];
success: {
workspaceId: string;
}[];
interrupted: boolean;
};
const DEFAULT_ACTIVATION_STATUSES = PROVISIONED_WORKSPACE_ACTIVATION_STATUSES;
@Injectable()
export class WorkspaceIteratorService {
private readonly logger = new Logger(WorkspaceIteratorService.name);
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly workspaceOrmManager: WorkspaceOrmManager,
private readonly commandShutdownService: CommandShutdownService,
) {}
listenToShutdownSignals(): void {
this.commandShutdownService.listenToShutdownSignals();
}
async iterate(args: WorkspaceIteratorArgs): Promise<WorkspaceIteratorReport> {
const { callback, ...options } = args;
const report: WorkspaceIteratorReport = {
fail: [],
success: [],
interrupted: false,
};
const workspaceIdsToProcess =
options.workspaceIds && options.workspaceIds.length > 0
? options.workspaceIds
: await this.fetchWorkspaceIds(options);
if (options.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
for (const [index, workspaceId] of workspaceIdsToProcess.entries()) {
if (this.commandShutdownService.isShutdownRequested()) {
this.logger.warn(
`Shutdown requested, stopping before workspace ${workspaceId}. ` +
`${workspaceIdsToProcess.length - index} workspace(s) left untouched.`,
);
report.interrupted = true;
break;
}
this.logger.log(
`Running on workspace ${workspaceId} ${index + 1}/${workspaceIdsToProcess.length}`,
);
try {
const authContext = buildSystemAuthContext(workspaceId);
await this.workspaceOrmManager.executeInWorkspaceContext(async () => {
const workspace = await this.workspaceRepository.findOne({
select: ['databaseSchema'],
where: { id: workspaceId },
});
const dataSource = isNonEmptyString(workspace?.databaseSchema)
? this.coreDataSource
: undefined;
if (!isDefined(dataSource)) {
this.logger.warn(
`Could not retrieve a workspace data source for workspace ${workspaceId} ` +
`(index ${index + 1}/${workspaceIdsToProcess.length}): ` +
`workspaceRowFound=${isDefined(workspace)}, ` +
`databaseSchema=${JSON.stringify(workspace?.databaseSchema ?? null)}`,
);
}
await callback({
workspaceId,
databaseSchema: workspace?.databaseSchema ?? undefined,
dataSource,
index,
total: workspaceIdsToProcess.length,
});
}, authContext);
report.success.push({ workspaceId });
} catch (error: unknown) {
report.fail.push({ error: error as Error, workspaceId });
}
}
report.fail.forEach(({ error, workspaceId }) => {
this.logger.error(
`Error in workspace ${workspaceId}: ${error.message}`,
error.stack,
);
if (error instanceof WorkspaceMigrationRunnerException && error.errors) {
for (const [label, innerError] of Object.entries(error.errors)) {
if (!isDefined(innerError)) continue;
if (innerError instanceof Error) {
this.logger.error(
`Caused by ${label} in workspace ${workspaceId}: ${innerError.message}`,
innerError.stack,
);
} else {
this.logger.error(
`Caused by ${label} in workspace ${workspaceId}: ${String(innerError)}`,
);
}
}
}
});
return report;
}
private async fetchWorkspaceIds(
options: Pick<
WorkspaceIteratorArgs,
| 'activationStatuses'
| 'startFromWorkspaceId'
| 'workspaceCountLimit'
| 'shard'
>,
): Promise<string[]> {
const activationStatuses =
options.activationStatuses ?? DEFAULT_ACTIVATION_STATUSES;
const shard = options.shard;
if (isDefined(shard)) {
if (isDefined(options.startFromWorkspaceId)) {
throw new Error(
'Cannot combine shard with startFromWorkspaceId in workspace iterator',
);
}
if (shard.index < 0 || shard.index >= shard.total || shard.total > 256) {
throw new Error(
`Invalid workspace iterator shard ${shard.index}/${shard.total}`,
);
}
}
const workspaces = await this.workspaceRepository.find({
select: ['id'],
where: {
activationStatus: activationStatusIn(activationStatuses),
...(options.startFromWorkspaceId
? { id: MoreThanOrEqual(options.startFromWorkspaceId) }
: {}),
...(isDefined(shard)
? {
id: Raw(
(alias) =>
`mod(get_byte(uuid_send(${alias}), 0), :shardTotal) = :shardIndex`,
{ shardTotal: shard.total, shardIndex: shard.index },
),
}
: {}),
},
order: { id: 'ASC' },
take: options.workspaceCountLimit,
});
return workspaces.map((workspace) => workspace.id);
}
}