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,
} from 'src/database/commands/base.command';
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 & {
workspaceId?: string;
startFromWorkspaceId?: string;
@ -19,7 +21,10 @@ export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner {
private startFromWorkspaceId: string | undefined;
private workspaceCountLimit: number | undefined;
constructor(protected readonly workspaceRepository: Repository<Workspace>) {
constructor(
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
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(
passedParams: string[],
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 { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
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_43CommandModule } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
@ -51,8 +49,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
DataSeedDemoWorkspaceModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataVersionModule,
UpgradeTo0_40CommandModule,
UpgradeTo0_41CommandModule,
UpgradeTo0_42CommandModule,
UpgradeTo0_43CommandModule,
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>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
async executeActiveWorkspacesCommand(

View File

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

View File

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

View File

@ -25,12 +25,13 @@ export class StandardizationOfActorCompositeContextTypeCommand extends ActiveWor
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
this.logger = new CommandLogger({
constructorName: this.constructor.name,
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 { 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 { 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';
type Upgrade042CommandCustomOptions = {
@ -25,13 +26,14 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly migrateRichTextFieldCommand: MigrateRichTextFieldCommand,
private readonly fixBodyV2ViewFieldPositionCommand: FixBodyV2ViewFieldPositionCommand,
private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly standardizationOfActorCompositeContextType: StandardizationOfActorCompositeContextTypeCommand,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
@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 { 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 { 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 { 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';
@ -37,11 +36,10 @@ export class AddTasksAssignedToMeViewCommand extends ActiveWorkspacesCommandRunn
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
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 { 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 { 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 { 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';
@ -26,6 +27,7 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveWorks
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
@ -34,7 +36,7 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveWorks
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
async executeActiveWorkspacesCommand(

View File

@ -29,9 +29,9 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
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',
);
for (const [index, workspaceId] of workspaceIds.entries()) {
await this.processWorkspace(workspaceId, index, workspaceIds.length);
}
this.processEachWorkspaceWithWorkspaceDataSource(
workspaceIds,
async ({ workspaceId, index, total }) => {
await this.processWorkspace(workspaceId, index, total);
},
);
this.logger.log(chalk.green('Command completed!'));
}
@ -87,14 +90,6 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
workspaceId,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
this.logger.log(
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 { 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade-0.43',
@ -19,12 +20,13 @@ export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
private readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
private readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
private readonly standardizationOfActorCompositeContextTypeCommand: StandardizationOfActorCompositeContextTypeCommand,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
async executeActiveWorkspacesCommand(

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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>;
@ -52,6 +52,9 @@ export class CacheManager<T> {
await onDelete?.(cachedValue);
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> {

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { Workspace } 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 { 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';
@ -30,8 +31,9 @@ export class SyncWorkspaceMetadataCommand extends ActiveWorkspacesCommandRunner
private readonly workspaceHealthService: WorkspaceHealthService,
private readonly dataSourceService: DataSourceService,
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
super(workspaceRepository, twentyORMGlobalManager);
}
async executeActiveWorkspacesCommand(