Provide a wrapper to execute command on workspace with easier devXP (#10391)

Proposal:
- Add a method in ActiveWorkspaceCommand to loop over workspace safely
(add counter, add try / catch, provide datasource with fresh cache,
destroy datasource => as we do always do it)

Also in this PR:
- make sure we clear all dataSources (and not only the one on metadata
version in RAM)
This commit is contained in:
Charles Bochet
2025-02-21 16:40:33 +01:00
committed by GitHub
parent 7a3e92fe0b
commit d747366bf3
27 changed files with 120 additions and 1393 deletions

View File

@ -8,6 +8,8 @@ import {
BaseCommandRunner, BaseCommandRunner,
} from 'src/database/commands/base.command'; } from 'src/database/commands/base.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export type ActiveWorkspacesCommandOptions = BaseCommandOptions & { export type ActiveWorkspacesCommandOptions = BaseCommandOptions & {
workspaceId?: string; workspaceId?: string;
startFromWorkspaceId?: string; startFromWorkspaceId?: string;
@ -19,7 +21,10 @@ export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner {
private startFromWorkspaceId: string | undefined; private startFromWorkspaceId: string | undefined;
private workspaceCountLimit: number | undefined; private workspaceCountLimit: number | undefined;
constructor(protected readonly workspaceRepository: Repository<Workspace>) { constructor(
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(); super();
} }
@ -122,6 +127,54 @@ export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner {
); );
} }
protected async processEachWorkspaceWithWorkspaceDataSource(
workspaceIds: string[],
callback: ({
workspaceId,
index,
total,
dataSource,
}: {
workspaceId: string;
index: number;
total: number;
dataSource: WorkspaceDataSource;
}) => Promise<void>,
): Promise<void> {
this.logger.log(
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
);
for (const [index, workspaceId] of workspaceIds.entries()) {
this.logger.log(
chalk.green(
`Processing workspace ${workspaceId} (${index + 1}/${
workspaceIds.length
})`,
),
);
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
workspaceId,
false,
);
try {
await callback({
workspaceId,
index,
total: workspaceIds.length,
dataSource,
});
} catch (error) {
this.logger.error(`Error in workspace ${workspaceId}: ${error}`);
}
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
}
protected abstract executeActiveWorkspacesCommand( protected abstract executeActiveWorkspacesCommand(
passedParams: string[], passedParams: string[],
options: BaseCommandOptions, options: BaseCommandOptions,

View File

@ -7,8 +7,6 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module'; import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command'; import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
import { UpgradeTo0_41CommandModule } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module';
import { UpgradeTo0_42CommandModule } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module'; import { UpgradeTo0_42CommandModule } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module';
import { UpgradeTo0_43CommandModule } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module'; import { UpgradeTo0_43CommandModule } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
@ -51,8 +49,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
DataSeedDemoWorkspaceModule, DataSeedDemoWorkspaceModule,
WorkspaceCacheStorageModule, WorkspaceCacheStorageModule,
WorkspaceMetadataVersionModule, WorkspaceMetadataVersionModule,
UpgradeTo0_40CommandModule,
UpgradeTo0_41CommandModule,
UpgradeTo0_42CommandModule, UpgradeTo0_42CommandModule,
UpgradeTo0_43CommandModule, UpgradeTo0_43CommandModule,
FeatureFlagModule, FeatureFlagModule,

View File

@ -1,245 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { isCommandLogger } from 'src/database/commands/logger';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
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 { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
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';
import {
VIEW_FIELD_STANDARD_FIELD_IDS,
VIEW_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@Command({
name: 'upgrade-0.40:migrate-aggregate-operation-options',
description: 'Add aggregate operations options to relevant fields',
})
export class MigrateAggregateOperationOptionsCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
}
ADDITIONAL_AGGREGATE_OPERATIONS = [
{
value: AGGREGATE_OPERATIONS.countEmpty,
label: 'Count empty',
position: 5,
color: 'red',
},
{
value: AGGREGATE_OPERATIONS.countNotEmpty,
label: 'Count not empty',
position: 6,
color: 'purple',
},
{
value: AGGREGATE_OPERATIONS.countUniqueValues,
label: 'Count unique values',
position: 7,
color: 'sky',
},
{
value: AGGREGATE_OPERATIONS.percentageEmpty,
label: 'Percent empty',
position: 8,
color: 'turquoise',
},
{
value: AGGREGATE_OPERATIONS.percentageNotEmpty,
label: 'Percent not empty',
position: 9,
color: 'yellow',
},
];
ADDITIONAL_AGGREGATE_OPERATIONS_VALUES =
this.ADDITIONAL_AGGREGATE_OPERATIONS.map((option) => option.value);
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to migrate aggregate operations options to include count operations',
);
if (isCommandLogger(this.logger)) {
this.logger.setVerbose(options.verbose ?? false);
}
let workspaceIterator = 1;
for (const workspaceId of workspaceIds) {
this.logger.log(
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
);
try {
const viewFieldObjectMetadata =
await this.objectMetadataRepository.findOne({
where: {
workspaceId,
standardId: STANDARD_OBJECT_IDS.viewField,
},
});
if (!isDefined(viewFieldObjectMetadata)) {
throw new Error(
`View field object metadata not found for workspace ${workspaceId}`,
);
}
const viewFieldAggregateOperationFieldMetadata =
await this.fieldMetadataRepository.findOne({
where: {
workspaceId,
objectMetadataId: viewFieldObjectMetadata.id,
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.aggregateOperation,
},
});
if (isDefined(viewFieldAggregateOperationFieldMetadata)) {
await this.updateAggregateOperationField(
workspaceId,
viewFieldAggregateOperationFieldMetadata,
viewFieldObjectMetadata,
);
}
const viewObjectMetadata = await this.objectMetadataRepository.findOne({
where: {
workspaceId,
standardId: STANDARD_OBJECT_IDS.view,
},
});
if (!isDefined(viewObjectMetadata)) {
throw new Error(
`View object metadata not found for workspace ${workspaceId}`,
);
}
const viewAggregateOperationFieldMetadata =
await this.fieldMetadataRepository.findOne({
where: {
workspaceId,
objectMetadataId: viewObjectMetadata.id,
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation,
},
});
if (isDefined(viewAggregateOperationFieldMetadata)) {
await this.updateAggregateOperationField(
workspaceId,
viewAggregateOperationFieldMetadata,
viewObjectMetadata,
);
}
if (
isDefined(viewAggregateOperationFieldMetadata) ||
isDefined(viewFieldAggregateOperationFieldMetadata)
) {
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
}
workspaceIterator++;
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
} catch {
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
workspaceIterator++;
}
}
this.logger.log(chalk.green(`Command completed!`));
}
private async updateAggregateOperationField(
workspaceId: string,
fieldMetadata: FieldMetadataEntity,
objectMetadata: ObjectMetadataEntity,
) {
if (
fieldMetadata.options.some((option) => {
return this.ADDITIONAL_AGGREGATE_OPERATIONS_VALUES.includes(
option.value as AGGREGATE_OPERATIONS,
);
})
) {
this.logger.log(
`Aggregate operation field metadata ${fieldMetadata.name} already has the required options`,
);
} else {
const updatedFieldMetadata = {
...fieldMetadata,
options: [
...fieldMetadata.options,
...this.ADDITIONAL_AGGREGATE_OPERATIONS.map((operation) => ({
...operation,
id: v4(),
})),
],
};
await this.fieldMetadataRepository.save(updatedFieldMetadata);
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`update-${objectMetadata.nameSingular}-aggregate-operation`,
),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.ALTER,
fieldMetadata,
updatedFieldMetadata,
),
} satisfies WorkspaceMigrationTableAction,
],
);
}
}
}

View File

@ -1,248 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command, Option } from 'nest-commander';
import { WorkspaceActivationStatus } from 'twenty-shared';
import { In, Repository } from 'typeorm';
import {
BaseCommandOptions,
BaseCommandRunner,
} from 'src/database/commands/base.command';
import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
type UpdateInactiveWorkspaceStatusOptions = BaseCommandOptions & {
workspaceIds: string[];
};
@Command({
name: 'upgrade-0.40:update-inactive-workspace-status',
description:
'Update the status of inactive workspaces to SUSPENDED and delete them',
})
export class UpdateInactiveWorkspaceStatusCommand extends BaseCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata')
protected readonly datasourceRepository: Repository<DataSourceEntity>,
@InjectRepository(WorkspaceMigrationEntity, 'metadata')
protected readonly workspaceMigrationRepository: Repository<WorkspaceMigrationEntity>,
@InjectRepository(BillingSubscription, 'core')
protected readonly subscriptionRepository: Repository<BillingSubscription>,
@InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>,
@InjectRepository(KeyValuePair, 'core')
protected readonly keyValuePairRepository: Repository<KeyValuePair>,
@InjectRepository(UserWorkspace, 'core')
protected readonly userWorkspaceRepository: Repository<UserWorkspace>,
@InjectRepository(User, 'core')
protected readonly userRepository: Repository<User>,
private readonly typeORMService: TypeORMService,
) {
super();
}
@Option({
flags: '-w, --workspace-ids [workspaceIds]',
description: 'Workspace ids to process (comma separated)',
})
parseWorkspaceIds(val: string): string[] {
return val.split(',');
}
override async executeBaseCommand(
_passedParams: string[],
options: UpdateInactiveWorkspaceStatusOptions,
): Promise<void> {
const whereCondition: any = {
activationStatus: WorkspaceActivationStatus.INACTIVE,
};
if (options.workspaceIds?.length > 0) {
whereCondition.id = In(options.workspaceIds);
}
const workspaces = await this.workspaceRepository.find({
where: whereCondition,
});
if (options.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
this.logger.log(
chalk.blue(
`Found ${workspaces.length} inactive workspace${
workspaces.length > 1 ? 's' : ''
}`,
),
);
await rawDataSource.initialize();
for (const workspace of workspaces) {
this.logger.log(
chalk.blue(
`Processing workspace ${workspace.id} with name ${workspace.displayName}`,
),
);
// Check if the workspace has a datasource
const datasource = await this.datasourceRepository.findOne({
where: { workspaceId: workspace.id },
});
const schemaName = datasource?.schema;
const postgresSchemaExists = await this.typeORMService
.getMainDataSource()
.query(
`SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '${schemaName}'`,
);
if (!schemaName || !postgresSchemaExists) {
await this.deleteWorkspaceAndMarkAsSuspended(workspace, options);
continue;
}
const subscriptions = await this.subscriptionRepository.find({
where: { workspaceId: workspace.id },
});
if (subscriptions.length > 1) {
this.logger.warn(chalk.red('More than one subscription found'));
continue;
}
const subscription = subscriptions[0];
if (!subscription) {
this.logger.log(chalk.red('No subscription found'));
await this.deleteWorkspaceAndMarkAsSuspendedAndDeleteAllData(
workspace,
schemaName,
options,
);
continue;
}
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
if (
([
SubscriptionStatus.Canceled,
SubscriptionStatus.Incomplete,
SubscriptionStatus.IncompleteExpired,
SubscriptionStatus.Unpaid,
SubscriptionStatus.Paused,
].includes(subscription.status) &&
subscription.canceledAt &&
subscription.canceledAt < thirtyDaysAgo) ||
(subscription.canceledAt === null &&
subscription.updatedAt &&
subscription.updatedAt < thirtyDaysAgo)
) {
await this.deleteWorkspaceAndMarkAsSuspendedAndDeleteAllData(
workspace,
schemaName,
options,
subscription,
);
continue;
}
await this.markAsSuspended(workspace, options);
}
}
private async deleteWorkspaceAndMarkAsSuspended(
workspace: Workspace,
options: UpdateInactiveWorkspaceStatusOptions,
) {
this.logger.log(
chalk.blue('(!!) Deleting workspace and marking as suspended'),
);
if (!options.dryRun) {
await this.workspaceRepository.update(workspace.id, {
activationStatus: WorkspaceActivationStatus.SUSPENDED,
});
await this.workspaceRepository.softRemove({ id: workspace.id });
}
}
private async deleteWorkspaceAndMarkAsSuspendedAndDeleteAllData(
workspace: Workspace,
schemaName: string,
options: UpdateInactiveWorkspaceStatusOptions,
billingSubscription?: BillingSubscription,
) {
this.logger.warn(
chalk.blue(
`(!!!) Deleting workspace and marking as suspended and deleting all data for workspace updated at ${workspace.updatedAt} with subscription status ${billingSubscription?.status} and subscription updatedAt ${billingSubscription?.updatedAt} and canceledAt ${billingSubscription?.canceledAt}`,
),
);
if (!options.dryRun) {
await this.workspaceRepository.update(workspace.id, {
activationStatus: WorkspaceActivationStatus.SUSPENDED,
});
await this.workspaceRepository.softRemove({ id: workspace.id });
await this.datasourceRepository.delete({ workspaceId: workspace.id });
await this.workspaceMigrationRepository.delete({
workspaceId: workspace.id,
});
await this.featureFlagRepository.delete({ workspaceId: workspace.id });
await this.keyValuePairRepository.delete({ workspaceId: workspace.id });
const userWorkspaces = await this.userWorkspaceRepository.find({
where: { workspaceId: workspace.id },
});
for (const userWorkspace of userWorkspaces) {
await this.userWorkspaceRepository.delete({ id: userWorkspace.id });
const remainingUserWorkspaces =
await this.userWorkspaceRepository.count({
where: { userId: userWorkspace.userId },
});
if (remainingUserWorkspaces === 0) {
await this.userRepository.softRemove({ id: userWorkspace.userId });
}
}
await this.typeORMService
.getMainDataSource()
.query(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
}
}
private async markAsSuspended(
workspace: Workspace,
options: UpdateInactiveWorkspaceStatusOptions,
) {
this.logger.log(chalk.blue('(!) Marking as suspended'));
if (!options.dryRun) {
await this.workspaceRepository.update(workspace.id, {
activationStatus: WorkspaceActivationStatus.SUSPENDED,
});
}
}
}

View File

@ -1,37 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Command({
name: 'upgrade-0.40',
description: 'Upgrade to 0.40',
})
export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly migrateAggregateOperationOptionsCommand: MigrateAggregateOperationOptionsCommand,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
passedParam: string[],
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log('Running command to upgrade to 0.40');
await this.migrateAggregateOperationOptionsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
}
}

View File

@ -1,55 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command';
import { UpdateInactiveWorkspaceStatusCommand } from 'src/database/commands/upgrade-version/0-40/0-40-update-inactive-workspace-status.command';
import { UpgradeTo0_40Command } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
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 { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
TypeOrmModule.forFeature(
[
Workspace,
BillingSubscription,
FeatureFlag,
KeyValuePair,
User,
UserWorkspace,
],
'core',
),
TypeOrmModule.forFeature(
[
ObjectMetadataEntity,
FieldMetadataEntity,
DataSourceEntity,
WorkspaceMigrationEntity,
],
'metadata',
),
WorkspaceMigrationRunnerModule,
WorkspaceMigrationModule,
WorkspaceMetadataVersionModule,
TypeORMModule,
],
providers: [
UpgradeTo0_40Command,
MigrateAggregateOperationOptionsCommand,
UpdateInactiveWorkspaceStatusCommand,
],
})
export class UpgradeTo0_40CommandModule {}

View File

@ -1,195 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { FieldMetadataType } from 'twenty-shared';
import { In, Repository, TableColumn } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { CommandLogger } from 'src/database/commands/logger';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Command({
name: 'upgrade-0.41:add-context-to-actor-composite-type',
description: 'Add context to actor composite type.',
})
export class AddContextToActorCompositeTypeCommand extends ActiveWorkspacesCommandRunner {
protected readonly logger;
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
this.logger = new CommandLogger({
constructorName: this.constructor.name,
verbose: false,
});
this.logger.setVerbose(false);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(`Running add-context-to-actor-composite-type command`);
if (options?.dryRun) {
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
}
for (const workspaceId of workspaceIds) {
try {
await this.execute(workspaceId, options?.dryRun);
this.logger.verbose(`Added for workspace: ${workspaceId}`);
} catch (error) {
this.logger.error(`Error for workspace: ${workspaceId}`, error);
}
}
}
private async execute(workspaceId: string, dryRun = false): Promise<void> {
this.logger.verbose(`Adding for workspace: ${workspaceId}`);
const actorFields = await this.fieldMetadataRepository.find({
where: {
type: FieldMetadataType.ACTOR,
workspaceId,
},
relations: ['object'],
});
// Filter and update fields with EMAIL or CALENDAR source
for (const field of actorFields) {
if (!field || !field.object) {
this.logger.verbose(
'field.objectMetadata is null',
workspaceId,
field.id,
);
continue;
}
await this.addContextColumn(
field,
`${field.name}Context`,
workspaceId,
dryRun,
);
const fieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
field.object.nameSingular,
);
if (!dryRun) {
const rowsToUpdate = await fieldRepository.update(
{
[field.name + 'Source']: In([
FieldActorSource.EMAIL,
FieldActorSource.CALENDAR,
]),
[field.name + 'Context']: {},
},
{
[field.name + 'Context']: {
provider: 'google',
},
},
);
this.logger.verbose(
`updated ${rowsToUpdate ? rowsToUpdate.affected : 0} rows`,
);
}
}
if (!dryRun) {
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
}
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
private async addContextColumn(
field: FieldMetadataEntity,
newColumnName: string,
workspaceId: string,
dryRun = false,
): Promise<void> {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
if (!workspaceDataSource) {
this.logger.verbose('No workspace data source found');
return;
}
const queryRunner = workspaceDataSource?.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);
try {
const hasColumn = await queryRunner.hasColumn(
`${schemaName}.${computeTableName(
field.object.nameSingular,
field?.object?.isCustom,
)}`,
newColumnName,
);
if (hasColumn) {
return;
}
if (!dryRun) {
await queryRunner.addColumn(
`${schemaName}.${computeTableName(
field.object.nameSingular,
field?.object?.isCustom,
)}`,
new TableColumn({
name: newColumnName,
type: 'jsonb',
default: `'{}'::"jsonb"`,
isNullable: true,
}),
);
await queryRunner.commitTransaction();
}
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
}

View File

@ -1,140 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { FieldMetadataType } from 'twenty-shared';
import { Repository } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { isCommandLogger } from 'src/database/commands/logger';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
deduceRelationDirection,
RelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
@Command({
name: 'upgrade-0.41:migrate-relations-to-field-metadata',
description: 'Migrate relations to field metadata',
})
export class MigrateRelationsToFieldMetadataCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log('Running command to create many to one relations');
if (isCommandLogger(this.logger)) {
this.logger.setVerbose(options.verbose ?? false);
}
try {
for (const [index, workspaceId] of workspaceIds.entries()) {
await this.processWorkspace(workspaceId, index, workspaceIds.length);
}
this.logger.log(chalk.green('Command completed!'));
} catch (error) {
this.logger.log(chalk.red('Error in workspace'));
}
}
private async processWorkspace(
workspaceId: string,
index: number,
total: number,
): Promise<void> {
try {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const fieldMetadataCollection = (await this.fieldMetadataRepository.find({
where: { workspaceId, type: FieldMetadataType.RELATION },
relations: ['fromRelationMetadata', 'toRelationMetadata'],
})) as unknown as FieldMetadataEntity<FieldMetadataType.RELATION>[];
if (!fieldMetadataCollection.length) {
this.logger.log(
chalk.yellow(
`No relation field metadata found for workspace ${workspaceId}.`,
),
);
return;
}
const fieldMetadataToUpdateCollection = fieldMetadataCollection.map(
(fieldMetadata) => this.mapFieldMetadata(fieldMetadata),
);
if (fieldMetadataToUpdateCollection.length > 0) {
await this.fieldMetadataRepository.save(
fieldMetadataToUpdateCollection,
);
}
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
} catch {
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
}
}
private mapFieldMetadata(
fieldMetadata: FieldMetadataEntity<FieldMetadataType.RELATION>,
): FieldMetadataEntity<FieldMetadataType.RELATION> {
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
const relationDirection = deduceRelationDirection(
fieldMetadata,
relationMetadata,
);
let relationType = relationMetadata.relationType as unknown as RelationType;
if (
relationDirection === RelationDirection.TO &&
relationType === RelationType.ONE_TO_MANY
) {
relationType = RelationType.MANY_TO_ONE;
}
const relationTargetFieldMetadataId =
relationDirection === RelationDirection.FROM
? relationMetadata.toFieldMetadataId
: relationMetadata.fromFieldMetadataId;
const relationTargetObjectMetadataId =
relationDirection === RelationDirection.FROM
? relationMetadata.toObjectMetadataId
: relationMetadata.fromObjectMetadataId;
return {
...fieldMetadata,
settings: {
relationType,
onDelete: relationMetadata.onDeleteAction,
},
relationTargetFieldMetadataId,
relationTargetObjectMetadataId,
};
}
}

View File

@ -1,98 +0,0 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade-0.41:remove-duplicate-mcmas',
description: 'Remove duplicate mcmas.',
})
export class RemoveDuplicateMcmasCommand extends ActiveWorkspacesCommandRunner {
protected readonly logger: Logger;
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
this.logger = new Logger(this.constructor.name);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
_options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
const { dryRun } = _options;
for (const workspaceId of workspaceIds) {
try {
await this.execute(workspaceId, dryRun);
} catch (error) {
this.logger.error(
`Error removing duplicate mcmas for workspace ${workspaceId}: ${error}`,
);
}
}
}
private async execute(workspaceId: string, dryRun = false): Promise<void> {
this.logger.log(`Removing duplicate mcmas for workspace: ${workspaceId}`);
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'messageChannelMessageAssociation',
);
const queryBuilder = repository.createQueryBuilder(
'messageChannelMessageAssociation',
);
const duplicateMcmas = await queryBuilder
.select(`"messageChannelId"`)
.addSelect(`"messageId"`)
.where(`"deletedAt" IS NULL`)
.groupBy(`"messageId"`)
.addGroupBy(`"messageChannelId"`)
.having(`COUNT("messageChannelId") > 1`)
.getRawMany();
this.logger.log(`Found ${duplicateMcmas.length} duplicate mcmas`);
for (const duplicateMca of duplicateMcmas) {
const mcmas = await repository.find({
where: {
messageId: duplicateMca.messageId,
messageChannelId: duplicateMca.messageChannelId,
},
});
this.logger.log(
`Found ${mcmas.length} mcmas for message ${duplicateMca.messageId} and message channel ${duplicateMca.messageChannelId}`,
);
const mcaIdsToDelete = mcmas.slice(1).map((mca) => mca.id);
if (mcaIdsToDelete.length > 0) {
this.logger.log(`Deleting ${mcaIdsToDelete.length} mcas`);
if (!dryRun) {
await repository.delete(mcaIdsToDelete);
}
}
}
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
}

View File

@ -1,207 +0,0 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { EntityManager, IsNull, Not, Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { createWorkspaceViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/create-workspace-views';
import { workflowRunsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view';
import { workflowVersionsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view';
import { workflowsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@Command({
name: 'upgrade-0.41:workflow-seed-views',
description: 'Seed workflow views for workspace.',
})
export class SeedWorkflowViewsCommand extends ActiveWorkspacesCommandRunner {
protected readonly logger: Logger;
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
private readonly objectMetadataService: ObjectMetadataService,
) {
super(workspaceRepository);
this.logger = new Logger(this.constructor.name);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
_options: ActiveWorkspacesCommandOptions,
_workspaceIds: string[],
): Promise<void> {
const { dryRun } = _options;
for (const workspaceId of _workspaceIds) {
await this.execute(workspaceId, dryRun);
}
}
private async execute(workspaceId: string, dryRun = false): Promise<void> {
this.logger.log(`Seeding workflow views for workspace: ${workspaceId}`);
const workflowObjectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
standardId: STANDARD_OBJECT_IDS.workflow,
},
});
if (!workflowObjectMetadata) {
this.logger.error('Workflow object metadata not found');
return;
}
await this.seedWorkflowViews(
workspaceId,
workflowObjectMetadata.id,
dryRun,
);
await this.seedWorkspaceFavorite(
workspaceId,
workflowObjectMetadata.id,
dryRun,
);
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
private async seedWorkflowViews(
workspaceId: string,
workflowObjectMetadataId: string,
dryRun = false,
) {
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'view',
);
const existingWorkflowView = await viewRepository.findOne({
where: {
objectMetadataId: workflowObjectMetadataId,
},
});
if (existingWorkflowView) {
this.logger.log(`View already exists: ${existingWorkflowView.id}`);
return;
}
if (dryRun) {
this.logger.log(`Dry run: not creating view`);
return;
}
const { objectMetadataStandardIdToIdMap } =
await this.objectMetadataService.getObjectMetadataStandardIdToIdMap(
workspaceId,
);
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
if (!workspaceDataSource) {
this.logger.error('Could not connect to workspace data source');
return;
}
const viewDefinitions = [
workflowsAllView(objectMetadataStandardIdToIdMap),
workflowVersionsAllView(objectMetadataStandardIdToIdMap),
workflowRunsAllView(objectMetadataStandardIdToIdMap),
];
await workspaceDataSource.transaction(
async (entityManager: EntityManager) => {
return createWorkspaceViews(
entityManager,
dataSourceMetadata.schema,
viewDefinitions,
);
},
);
}
private async seedWorkspaceFavorite(
workspaceId: string,
workflowObjectMetadataId: string,
dryRun = false,
) {
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'view',
);
const workflowView = await viewRepository.findOne({
where: {
objectMetadataId: workflowObjectMetadataId,
},
});
if (!workflowView) {
this.logger.error('Workflow view not found');
return;
}
if (dryRun) {
this.logger.log(`Dry run: not creating favorite`);
return;
}
const favoriteRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'favorite',
);
const existingFavorites = await favoriteRepository.find({
where: {
viewId: Not(IsNull()),
},
});
const workflowFavorite = existingFavorites.find(
(favorite) => favorite.viewId === workflowView.id,
);
if (workflowFavorite) {
this.logger.log(`Favorite already exists: ${workflowFavorite.id}`);
return;
}
await favoriteRepository.insert({
viewId: workflowView.id,
position: existingFavorites.length,
});
}
}

View File

@ -1,72 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { AddContextToActorCompositeTypeCommand } from 'src/database/commands/upgrade-version/0-41/0-41-add-context-to-actor-composite-type';
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command';
import { RemoveDuplicateMcmasCommand } from 'src/database/commands/upgrade-version/0-41/0-41-remove-duplicate-mcmas';
import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
@Command({
name: 'upgrade-0.41',
description: 'Upgrade to 0.41',
})
export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly seedWorkflowViewsCommand: SeedWorkflowViewsCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly migrateRelationsToFieldMetadata: MigrateRelationsToFieldMetadataCommand,
private readonly addContextToActorCompositeType: AddContextToActorCompositeTypeCommand,
private readonly removeDuplicateMcmasCommand: RemoveDuplicateMcmasCommand,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
passedParam: string[],
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log('Running command to upgrade to 0.41');
await this.removeDuplicateMcmasCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.addContextToActorCompositeType.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
passedParam,
{
...options,
force: true,
},
workspaceIds,
);
await this.seedWorkflowViewsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.migrateRelationsToFieldMetadata.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
}
}

View File

@ -1,45 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddContextToActorCompositeTypeCommand } from 'src/database/commands/upgrade-version/0-41/0-41-add-context-to-actor-composite-type';
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command';
import { RemoveDuplicateMcmasCommand } from 'src/database/commands/upgrade-version/0-41/0-41-remove-duplicate-mcmas';
import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command';
import { UpgradeTo0_41Command } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
TypeORMModule,
DataSourceModule,
ObjectMetadataModule,
WorkspaceSyncMetadataCommandsModule,
WorkspaceSyncMetadataModule,
WorkspaceHealthModule,
WorkspaceDataSourceModule,
WorkspaceMetadataVersionModule,
],
providers: [
SyncWorkspaceLoggerService,
SyncWorkspaceMetadataCommand,
SeedWorkflowViewsCommand,
UpgradeTo0_41Command,
MigrateRelationsToFieldMetadataCommand,
AddContextToActorCompositeTypeCommand,
RemoveDuplicateMcmasCommand,
],
})
export class UpgradeTo0_41CommandModule {}

View File

@ -26,10 +26,10 @@ export class FixBodyV2ViewFieldPositionCommand extends ActiveWorkspacesCommandRu
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(ObjectMetadataEntity, 'metadata') @InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(

View File

@ -25,9 +25,9 @@ export class LimitAmountOfViewFieldCommand extends ActiveWorkspacesCommandRunner
constructor( constructor(
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
this.logger = new CommandLogger({ this.logger = new CommandLogger({
constructorName: this.constructor.name, constructorName: this.constructor.name,
verbose: false, verbose: false,

View File

@ -64,6 +64,7 @@ export class MigrateRichTextFieldCommand extends ActiveWorkspacesCommandRunner {
constructor( constructor(
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata') @InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata') @InjectRepository(ObjectMetadataEntity, 'metadata')
@ -71,12 +72,11 @@ export class MigrateRichTextFieldCommand extends ActiveWorkspacesCommandRunner {
@InjectRepository(FeatureFlag, 'core') @InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>, protected readonly featureFlagRepository: Repository<FeatureFlag>,
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
@Option({ @Option({

View File

@ -25,12 +25,13 @@ export class StandardizationOfActorCompositeContextTypeCommand extends ActiveWor
constructor( constructor(
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata') @InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
this.logger = new CommandLogger({ this.logger = new CommandLogger({
constructorName: this.constructor.name, constructorName: this.constructor.name,
verbose: false, verbose: false,

View File

@ -10,6 +10,7 @@ import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-ver
import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command'; import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command';
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type'; import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
type Upgrade042CommandCustomOptions = { type Upgrade042CommandCustomOptions = {
@ -25,13 +26,14 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner {
constructor( constructor(
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly migrateRichTextFieldCommand: MigrateRichTextFieldCommand, private readonly migrateRichTextFieldCommand: MigrateRichTextFieldCommand,
private readonly fixBodyV2ViewFieldPositionCommand: FixBodyV2ViewFieldPositionCommand, private readonly fixBodyV2ViewFieldPositionCommand: FixBodyV2ViewFieldPositionCommand,
private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand, private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly standardizationOfActorCompositeContextType: StandardizationOfActorCompositeContextTypeCommand, private readonly standardizationOfActorCompositeContextType: StandardizationOfActorCompositeContextTypeCommand,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
@Option({ @Option({

View File

@ -17,7 +17,6 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me'; import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
@ -37,11 +36,10 @@ export class AddTasksAssignedToMeViewCommand extends ActiveWorkspacesCommandRunn
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata') @InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(

View File

@ -14,6 +14,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchService } from 'src/engine/metadata-modules/search/search.service'; import { SearchService } from 'src/engine/metadata-modules/search/search.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity'; import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity'; import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
@ -26,6 +27,7 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveWorks
constructor( constructor(
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FeatureFlag, 'core') @InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>, protected readonly featureFlagRepository: Repository<FeatureFlag>,
@InjectRepository(ObjectMetadataEntity, 'metadata') @InjectRepository(ObjectMetadataEntity, 'metadata')
@ -34,7 +36,7 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveWorks
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(

View File

@ -29,9 +29,9 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(
@ -43,9 +43,12 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
'Running command to update default view record opening on workflow objects to record page', 'Running command to update default view record opening on workflow objects to record page',
); );
for (const [index, workspaceId] of workspaceIds.entries()) { this.processEachWorkspaceWithWorkspaceDataSource(
await this.processWorkspace(workspaceId, index, workspaceIds.length); workspaceIds,
} async ({ workspaceId, index, total }) => {
await this.processWorkspace(workspaceId, index, total);
},
);
this.logger.log(chalk.green('Command completed!')); this.logger.log(chalk.green('Command completed!'));
} }
@ -87,14 +90,6 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
workspaceId, workspaceId,
); );
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
this.logger.log( this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`), chalk.green(`Command completed for workspace ${workspaceId}.`),
); );

View File

@ -10,6 +10,7 @@ import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-v
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command'; import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command'; import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({ @Command({
name: 'upgrade-0.43', name: 'upgrade-0.43',
@ -19,12 +20,13 @@ export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner {
constructor( constructor(
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>, protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand, private readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
private readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand, private readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
private readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand, private readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
private readonly standardizationOfActorCompositeContextTypeCommand: StandardizationOfActorCompositeContextTypeCommand, private readonly standardizationOfActorCompositeContextTypeCommand: StandardizationOfActorCompositeContextTypeCommand,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(

View File

@ -13,6 +13,7 @@ import {
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
interface SyncCustomerDataCommandOptions interface SyncCustomerDataCommandOptions
extends ActiveWorkspacesCommandOptions {} extends ActiveWorkspacesCommandOptions {}
@ -28,8 +29,9 @@ export class BillingSyncCustomerDataCommand extends ActiveWorkspacesCommandRunne
private readonly stripeSubscriptionService: StripeSubscriptionService, private readonly stripeSubscriptionService: StripeSubscriptionService,
@InjectRepository(BillingCustomer, 'core') @InjectRepository(BillingCustomer, 'core')
protected readonly billingCustomerRepository: Repository<BillingCustomer>, protected readonly billingCustomerRepository: Repository<BillingCustomer>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(

View File

@ -14,13 +14,17 @@ import {
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage'; import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage';
import { CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
export class WorkspaceDatasourceFactory { export class WorkspaceDatasourceFactory {
private readonly logger = new Logger(WorkspaceDatasourceFactory.name); private readonly logger = new Logger(WorkspaceDatasourceFactory.name);
private cacheManager = new CacheManager<WorkspaceDataSource>(); private cacheManager = new CacheManager<WorkspaceDataSource>();
private cachedDatasourcePromise: Record<string, Promise<WorkspaceDataSource>>; private cachedDataSourcePromise: Record<
CacheKey,
Promise<WorkspaceDataSource>
>;
constructor( constructor(
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
@ -29,7 +33,7 @@ export class WorkspaceDatasourceFactory {
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly entitySchemaFactory: EntitySchemaFactory, private readonly entitySchemaFactory: EntitySchemaFactory,
) { ) {
this.cachedDatasourcePromise = {}; this.cachedDataSourcePromise = {};
} }
public async create( public async create(
@ -53,16 +57,16 @@ export class WorkspaceDatasourceFactory {
); );
} }
const cacheKey = `${workspaceId}-${cachedWorkspaceMetadataVersion}`; const cacheKey: CacheKey = `${workspaceId}-${cachedWorkspaceMetadataVersion}`;
if (cacheKey in this.cachedDatasourcePromise) { if (cacheKey in this.cachedDataSourcePromise) {
return this.cachedDatasourcePromise[cacheKey]; return this.cachedDataSourcePromise[cacheKey];
} }
const creationPromise = (async (): Promise<WorkspaceDataSource> => { const creationPromise = (async (): Promise<WorkspaceDataSource> => {
try { try {
const result = await this.cacheManager.execute( const result = await this.cacheManager.execute(
cacheKey as '`${string}-${string}`', cacheKey,
async () => { async () => {
this.logger.log( this.logger.log(
`Creating workspace data source for workspace ${workspaceId} and metadata version ${cachedWorkspaceMetadataVersion}`, `Creating workspace data source for workspace ${workspaceId} and metadata version ${cachedWorkspaceMetadataVersion}`,
@ -178,22 +182,23 @@ export class WorkspaceDatasourceFactory {
return result; return result;
} finally { } finally {
delete this.cachedDatasourcePromise[cacheKey]; delete this.cachedDataSourcePromise[cacheKey];
} }
})(); })();
this.cachedDatasourcePromise[cacheKey] = creationPromise; this.cachedDataSourcePromise[cacheKey] = creationPromise;
return creationPromise; return creationPromise;
} }
public async destroy(workspaceId: string): Promise<void> { public async destroy(workspaceId: string): Promise<void> {
const cachedWorkspaceMetadataVersion = const cacheKeys = (
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); Object.keys(this.cachedDataSourcePromise) as CacheKey[]
).filter((key) => key.startsWith(`${workspaceId}`));
await this.cacheManager.clearKey( for (const cacheKey of cacheKeys) {
`${workspaceId}-${cachedWorkspaceMetadataVersion}`, await this.cacheManager.clearKey(cacheKey);
); }
} }
private async getWorkspaceMetadataVersionFromCache( private async getWorkspaceMetadataVersionFromCache(

View File

@ -1,6 +1,6 @@
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
type CacheKey = `${string}-${string}`; import { CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type';
type AsyncFactoryCallback<T> = () => Promise<T | null>; type AsyncFactoryCallback<T> = () => Promise<T | null>;
@ -52,6 +52,9 @@ export class CacheManager<T> {
await onDelete?.(cachedValue); await onDelete?.(cachedValue);
this.cache.delete(cacheKey); this.cache.delete(cacheKey);
} }
// TODO: remove this once we have debug on prod
// eslint-disable-next-line no-console
console.log('Datasource cache size: ', this.cache.size);
} }
async clear(onDelete?: (value: T) => Promise<void> | void): Promise<void> { async clear(onDelete?: (value: T) => Promise<void> | void): Promise<void> {

View File

@ -0,0 +1 @@
export type CacheKey = `${string}-${string}`;

View File

@ -50,8 +50,15 @@ export class TwentyORMGlobalManager {
return repository; return repository;
} }
async getDataSourceForWorkspace(workspaceId: string) { async getDataSourceForWorkspace(
return await this.workspaceDataSourceFactory.create(workspaceId, null); workspaceId: string,
failOnMetadataCacheMiss = true,
) {
return await this.workspaceDataSourceFactory.create(
workspaceId,
null,
failOnMetadataCacheMiss,
);
} }
async destroyDataSourceForWorkspace(workspaceId: string) { async destroyDataSourceForWorkspace(workspaceId: string) {

View File

@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service'; import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@ -30,8 +31,9 @@ export class SyncWorkspaceMetadataCommand extends ActiveWorkspacesCommandRunner
private readonly workspaceHealthService: WorkspaceHealthService, private readonly workspaceHealthService: WorkspaceHealthService,
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService, private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) { ) {
super(workspaceRepository); super(workspaceRepository, twentyORMGlobalManager);
} }
async executeActiveWorkspacesCommand( async executeActiveWorkspacesCommand(