Add command to tag workspace as suspended or as deleted (#9610)
In this PR: - remove old versions upgrade commands - add a 0.40 upgrade command to loop over all INACTIVE workspaces and either: update to SUSPENDED (if workspaceSchema exists), update them to SUSPENDED + deletedAt (if workspaceSchema does not exist anymore) Note: why updating the deleted one to SUSPENDED? Because I plan to remove INACTIVE case in the enum in 0.41 Tests made on production like database: - dry-mode - singleWorkspaceId - 3 cases : suspended, deleted+suspended, deleted+suspended+delete all data
This commit is contained in:
@ -7,10 +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_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module';
|
||||
import { UpgradeTo0_33CommandModule } from 'src/database/commands/upgrade-version/0-33/0-33-upgrade-version.module';
|
||||
import { UpgradeTo0_34CommandModule } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.module';
|
||||
import { UpgradeTo0_35CommandModule } from 'src/database/commands/upgrade-version/0-35/0-35-upgrade-version.module';
|
||||
import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
@ -52,10 +48,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
UpgradeTo0_32CommandModule,
|
||||
UpgradeTo0_33CommandModule,
|
||||
UpgradeTo0_34CommandModule,
|
||||
UpgradeTo0_35CommandModule,
|
||||
UpgradeTo0_40CommandModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
|
||||
@ -28,12 +28,12 @@ export class CommandLogger {
|
||||
this.logger.error(message, stack, context);
|
||||
}
|
||||
|
||||
warn(message: string, context?: string) {
|
||||
this.logger.warn(message, context);
|
||||
warn(message: string, ...optionalParams: [...any, string?]) {
|
||||
this.logger.warn(message, ...optionalParams);
|
||||
}
|
||||
|
||||
debug(message: string, context?: string) {
|
||||
this.logger.debug(message, context);
|
||||
debug(message: string, ...optionalParams: [...any, string?]) {
|
||||
this.logger.debug(message, ...optionalParams);
|
||||
}
|
||||
|
||||
verbose(message: string, ...optionalParams: [...any, string?]) {
|
||||
|
||||
@ -1,127 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.32:backfill-view-groups',
|
||||
description: 'Backfill view groups',
|
||||
})
|
||||
export class BackfillViewGroupsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix backfill view groups');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
const viewGroupRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewGroup',
|
||||
);
|
||||
|
||||
const kanbanViews = await viewRepository.find({
|
||||
where: {
|
||||
type: 'kanban',
|
||||
},
|
||||
});
|
||||
|
||||
const kanbanFieldMetadataIds = kanbanViews.map(
|
||||
(view) => view.kanbanFieldMetadataId,
|
||||
);
|
||||
|
||||
const kanbanFieldMetadataItems =
|
||||
await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
id: In(kanbanFieldMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
for (const kanbanView of kanbanViews) {
|
||||
const kanbanFieldMetadataItem = kanbanFieldMetadataItems.find(
|
||||
(item) => item.id === kanbanView.kanbanFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!kanbanFieldMetadataItem) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Kanban field metadata with id ${kanbanView.kanbanFieldMetadataId} not found`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const option of kanbanFieldMetadataItem.options) {
|
||||
const viewGroup = await viewGroupRepository.findOne({
|
||||
where: {
|
||||
fieldMetadataId: kanbanFieldMetadataItem.id,
|
||||
fieldValue: option.value,
|
||||
viewId: kanbanView.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (viewGroup) {
|
||||
this.logger.log(
|
||||
chalk.red(`View group with id ${option.value} already exists`),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await viewGroupRepository.save({
|
||||
fieldMetadataId: kanbanFieldMetadataItem.id,
|
||||
fieldValue: option.value,
|
||||
isVisible: true,
|
||||
viewId: kanbanView.id,
|
||||
position: option.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import chalk from 'chalk';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.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.32:copy-webhook-operation-into-operations',
|
||||
description:
|
||||
'Read, transform and copy webhook from deprecated column operation into newly created column operations',
|
||||
})
|
||||
export class CopyWebhookOperationIntoOperationsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParams: string[],
|
||||
options: BaseCommandOptions,
|
||||
activeWorkspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to copy operation to operations');
|
||||
|
||||
for (const workspaceId of activeWorkspaceIds) {
|
||||
try {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
const webhookRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'webhook',
|
||||
);
|
||||
|
||||
const webhooks = await webhookRepository.find();
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
if ('operation' in webhook) {
|
||||
let newOpe = webhook.operation;
|
||||
|
||||
newOpe = newOpe.replace(/\bcreate\b(?=\.|$)/g, 'created');
|
||||
newOpe = newOpe.replace(/\bupdate\b(?=\.|$)/g, 'updated');
|
||||
newOpe = newOpe.replace(/\bdelete\b(?=\.|$)/g, 'deleted');
|
||||
newOpe = newOpe.replace(/\bdestroy\b(?=\.|$)/g, 'destroyed');
|
||||
|
||||
const [firstWebhookPart, lastWebhookPart] = newOpe.split('.');
|
||||
|
||||
if (
|
||||
['created', 'updated', 'deleted', 'destroyed'].includes(
|
||||
firstWebhookPart,
|
||||
)
|
||||
) {
|
||||
newOpe = `${lastWebhookPart}.${firstWebhookPart}`;
|
||||
}
|
||||
|
||||
await webhookRepository.update(webhook.id, {
|
||||
operation: newOpe,
|
||||
operations: [newOpe],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Handled webhook operation updates for ${webhook.id}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Error when running command on workspace ${workspaceId}: ${e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,112 +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 {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { SearchService } from 'src/engine/metadata-modules/search/search.service';
|
||||
import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import {
|
||||
COMPANY_STANDARD_FIELD_IDS,
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS,
|
||||
PERSON_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { SEARCH_FIELDS_FOR_COMPANY } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||
import { SEARCH_FIELDS_FOR_OPPORTUNITY } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||
import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'fix-0.32:simplify-search-vector-expression',
|
||||
description: 'Replace searchVector with simpler expression',
|
||||
})
|
||||
export class SimplifySearchVectorExpressionCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly searchService: SearchService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix migration');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
const searchVectorFields = await this.fieldMetadataRepository.findBy({
|
||||
workspaceId: workspaceId,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
});
|
||||
|
||||
for (const searchVectorField of searchVectorFields) {
|
||||
let fieldsUsedForSearch: FieldTypeAndNameMetadata[] = [];
|
||||
|
||||
switch (searchVectorField.standardId) {
|
||||
case CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector: {
|
||||
fieldsUsedForSearch = SEARCH_FIELDS_FOR_CUSTOM_OBJECT;
|
||||
break;
|
||||
}
|
||||
case PERSON_STANDARD_FIELD_IDS.searchVector: {
|
||||
fieldsUsedForSearch = SEARCH_FIELDS_FOR_PERSON;
|
||||
break;
|
||||
}
|
||||
case COMPANY_STANDARD_FIELD_IDS.searchVector: {
|
||||
fieldsUsedForSearch = SEARCH_FIELDS_FOR_COMPANY;
|
||||
break;
|
||||
}
|
||||
case OPPORTUNITY_STANDARD_FIELD_IDS.searchVector: {
|
||||
fieldsUsedForSearch = SEARCH_FIELDS_FOR_OPPORTUNITY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldsUsedForSearch.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.searchService.updateSearchVector(
|
||||
searchVectorField.objectMetadataId,
|
||||
fieldsUsedForSearch,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,65 +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 { BackfillViewGroupsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-backfill-view-groups.command';
|
||||
import { CopyWebhookOperationIntoOperationsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command';
|
||||
import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression';
|
||||
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';
|
||||
|
||||
interface UpdateTo0_32CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.32',
|
||||
description: 'Upgrade to 0.32',
|
||||
})
|
||||
export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly simplifySearchVectorExpressionCommand: SimplifySearchVectorExpressionCommand,
|
||||
private readonly copyWebhookOperationIntoOperationsCommand: CopyWebhookOperationIntoOperationsCommand,
|
||||
private readonly backfillViewGroupsCommand: BackfillViewGroupsCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: UpdateTo0_32CommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
force: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.simplifySearchVectorExpressionCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.copyWebhookOperationIntoOperationsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.backfillViewGroupsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CopyWebhookOperationIntoOperationsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command';
|
||||
import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression';
|
||||
import { UpgradeTo0_32Command } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command';
|
||||
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 { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
import { BackfillViewGroupsCommand } from './0-32-backfill-view-groups.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
SearchModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeTo0_32Command,
|
||||
BackfillViewGroupsCommand,
|
||||
CopyWebhookOperationIntoOperationsCommand,
|
||||
SimplifySearchVectorExpressionCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_32CommandModule {}
|
||||
@ -1,95 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { IsNull, 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';
|
||||
|
||||
interface DeleteViewFieldsWithoutViewsCommandOptions
|
||||
extends ActiveWorkspacesCommandOptions {}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.33:delete-view-fields-without-views',
|
||||
description: 'Delete ViewFields that do not have a View',
|
||||
})
|
||||
export class DeleteViewFieldsWithoutViewsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: DeleteViewFieldsWithoutViewsCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to delete ViewFields that do not have a View',
|
||||
);
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
await this.deleteViewFieldsWithoutViewsForWorkspace(
|
||||
workspaceId,
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async deleteViewFieldsWithoutViewsForWorkspace(
|
||||
workspaceId: string,
|
||||
options: DeleteViewFieldsWithoutViewsCommandOptions,
|
||||
): Promise<void> {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
false,
|
||||
);
|
||||
|
||||
const viewFieldsWithoutViews = await viewFieldRepository.find({
|
||||
where: {
|
||||
viewId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const viewFieldIds = viewFieldsWithoutViews.map((vf) => vf.id);
|
||||
|
||||
if (!options.dryRun && viewFieldIds.length > 0) {
|
||||
await viewFieldRepository.delete(viewFieldIds);
|
||||
}
|
||||
|
||||
if (options.verbose) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Deleted ${viewFieldsWithoutViews.length} ViewFields that do not have a View`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,320 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { IsNull, 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';
|
||||
|
||||
interface EnforceUniqueConstraintsCommandOptions
|
||||
extends ActiveWorkspacesCommandOptions {
|
||||
person?: boolean;
|
||||
company?: boolean;
|
||||
viewField?: boolean;
|
||||
viewSort?: boolean;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.33:enforce-unique-constraints',
|
||||
description:
|
||||
'Enforce unique constraints on company domainName, person emailsPrimaryEmail, ViewField, and ViewSort',
|
||||
})
|
||||
export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--person',
|
||||
description: 'Enforce unique constraints on person emailsPrimaryEmail',
|
||||
})
|
||||
parsePerson() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--company',
|
||||
description: 'Enforce unique constraints on company domainName',
|
||||
})
|
||||
parseCompany() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--view-field',
|
||||
description: 'Enforce unique constraints on ViewField',
|
||||
})
|
||||
parseViewField() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--view-sort',
|
||||
description: 'Enforce unique constraints on ViewSort',
|
||||
})
|
||||
parseViewSort() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to enforce unique constraints');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
await this.enforceUniqueConstraintsForWorkspace(workspaceId, options);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async enforceUniqueConstraintsForWorkspace(
|
||||
workspaceId: string,
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
): Promise<void> {
|
||||
if (options.person) {
|
||||
await this.enforceUniquePersonEmail(workspaceId, options);
|
||||
}
|
||||
if (options.company) {
|
||||
await this.enforceUniqueCompanyDomainName(workspaceId, options);
|
||||
}
|
||||
if (options.viewField) {
|
||||
await this.enforceUniqueViewField(workspaceId, options);
|
||||
}
|
||||
if (options.viewSort) {
|
||||
await this.enforceUniqueViewSort(workspaceId, options);
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueCompanyDomainName(
|
||||
workspaceId: string,
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
): Promise<void> {
|
||||
const companyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'company',
|
||||
false,
|
||||
);
|
||||
|
||||
const duplicates = await companyRepository
|
||||
.createQueryBuilder('company')
|
||||
.select('company.domainNamePrimaryLinkUrl')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('company.deletedAt IS NULL')
|
||||
.where('company.domainNamePrimaryLinkUrl IS NOT NULL')
|
||||
.where("company.domainNamePrimaryLinkUrl != ''")
|
||||
.groupBy('company.domainNamePrimaryLinkUrl')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { company_domainNamePrimaryLinkUrl } = duplicate;
|
||||
const companies = await companyRepository.find({
|
||||
where: {
|
||||
domainName: {
|
||||
primaryLinkUrl: company_domainNamePrimaryLinkUrl,
|
||||
},
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < companies.length; i++) {
|
||||
const newdomainNamePrimaryLinkUrl = `${company_domainNamePrimaryLinkUrl}${i}`;
|
||||
|
||||
if (!options.dryRun) {
|
||||
await companyRepository.update(companies[i].id, {
|
||||
domainNamePrimaryLinkUrl: newdomainNamePrimaryLinkUrl,
|
||||
});
|
||||
}
|
||||
if (options.verbose) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Updated company ${companies[i].id} domainName from ${company_domainNamePrimaryLinkUrl} to ${newdomainNamePrimaryLinkUrl}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniquePersonEmail(
|
||||
workspaceId: string,
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
): Promise<void> {
|
||||
const personRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'person',
|
||||
false,
|
||||
);
|
||||
|
||||
const duplicates = await personRepository
|
||||
.createQueryBuilder('person')
|
||||
.select('person.emailsPrimaryEmail')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('person.deletedAt IS NULL')
|
||||
.where('person.emailsPrimaryEmail IS NOT NULL')
|
||||
.where("person.emailsPrimaryEmail != ''")
|
||||
.groupBy('person.emailsPrimaryEmail')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { person_emailsPrimaryEmail } = duplicate;
|
||||
const persons = await personRepository.find({
|
||||
where: {
|
||||
emails: {
|
||||
primaryEmail: person_emailsPrimaryEmail,
|
||||
},
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < persons.length; i++) {
|
||||
const newEmail = person_emailsPrimaryEmail?.includes('@')
|
||||
? `${person_emailsPrimaryEmail.split('@')[0]}+${i}@${person_emailsPrimaryEmail.split('@')[1]}`
|
||||
: `${person_emailsPrimaryEmail}+${i}`;
|
||||
|
||||
if (!options.dryRun) {
|
||||
await personRepository.update(persons[i].id, {
|
||||
emailsPrimaryEmail: newEmail,
|
||||
});
|
||||
}
|
||||
if (options.verbose) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Updated person ${persons[i].id} emailsPrimaryEmail from ${person_emailsPrimaryEmail} to ${newEmail}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueViewField(
|
||||
workspaceId: string,
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
): Promise<void> {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
false,
|
||||
);
|
||||
|
||||
const duplicates = await viewFieldRepository
|
||||
.createQueryBuilder('viewField')
|
||||
.select(['viewField.fieldMetadataId', 'viewField.viewId'])
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('viewField.deletedAt IS NULL')
|
||||
.groupBy('viewField.fieldMetadataId, viewField.viewId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const {
|
||||
viewField_fieldMetadataId: fieldMetadataId,
|
||||
viewField_viewId: viewId,
|
||||
} = duplicate;
|
||||
const viewFields = await viewFieldRepository.find({
|
||||
where: {
|
||||
fieldMetadataId,
|
||||
viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < viewFields.length; i++) {
|
||||
if (!options.dryRun) {
|
||||
await viewFieldRepository.softDelete(viewFields[i].id);
|
||||
}
|
||||
if (options.verbose) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Soft deleted duplicate ViewField ${viewFields[i].id} for fieldMetadataId ${fieldMetadataId} and viewId ${viewId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueViewSort(
|
||||
workspaceId: string,
|
||||
options: EnforceUniqueConstraintsCommandOptions,
|
||||
): Promise<void> {
|
||||
const viewSortRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewSort',
|
||||
false,
|
||||
);
|
||||
|
||||
const duplicates = await viewSortRepository
|
||||
.createQueryBuilder('viewSort')
|
||||
.select(['viewSort.fieldMetadataId', 'viewSort.viewId'])
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('viewSort.deletedAt IS NULL')
|
||||
.groupBy('viewSort.fieldMetadataId, viewSort.viewId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const {
|
||||
viewSort_fieldMetadataId: fieldMetadataId,
|
||||
viewSort_viewId: viewId,
|
||||
} = duplicate;
|
||||
const viewSorts = await viewSortRepository.find({
|
||||
where: {
|
||||
fieldMetadataId,
|
||||
viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < viewSorts.length; i++) {
|
||||
if (!options.dryRun) {
|
||||
await viewSortRepository.softDelete(viewSorts[i].id);
|
||||
}
|
||||
if (options.verbose) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Soft deleted duplicate ViewSort ${viewSorts[i].id} for fieldMetadataId ${fieldMetadataId} and viewId ${viewId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
|
||||
interface SetMissingLabelIdentifierToCustomObjectsCommandOptions
|
||||
extends ActiveWorkspacesCommandOptions {}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.33:set-missing-label-identifier-to-custom-objects',
|
||||
description: 'Set missing labelIdentifier to custom objects',
|
||||
})
|
||||
export class SetMissingLabelIdentifierToCustomObjectsCommand 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 twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: SetMissingLabelIdentifierToCustomObjectsCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to set missing labelIdentifier to custom objects',
|
||||
);
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
await this.setMissingLabelIdentifierToCustomObjectsForWorkspace(
|
||||
workspaceId,
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async setMissingLabelIdentifierToCustomObjectsForWorkspace(
|
||||
workspaceId: string,
|
||||
options: SetMissingLabelIdentifierToCustomObjectsCommandOptions,
|
||||
): Promise<void> {
|
||||
const customObjects = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
labelIdentifierFieldMetadataId: IsNull(),
|
||||
isCustom: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const customObject of customObjects) {
|
||||
const labelIdentifierFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
objectMetadataId: customObject.id,
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
},
|
||||
});
|
||||
|
||||
if (labelIdentifierFieldMetadata && !options.dryRun) {
|
||||
await this.objectMetadataRepository.update(customObject.id, {
|
||||
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.verbose) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Set labelIdentifierFieldMetadataId for custom object ${customObject.nameSingular}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,107 +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 {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { SearchService } from 'src/engine/metadata-modules/search/search.service';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import {
|
||||
NOTE_STANDARD_FIELD_IDS,
|
||||
TASK_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||
import { SEARCH_FIELDS_FOR_TASK } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'fix-0.33:update-rich-text-expression',
|
||||
description: 'Update rich text (note and task) search vector expressions',
|
||||
})
|
||||
export class UpdateRichTextSearchVectorCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly searchService: SearchService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to fix migration');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
|
||||
try {
|
||||
const searchVectorFields = await this.fieldMetadataRepository.findBy({
|
||||
workspaceId: workspaceId,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
});
|
||||
|
||||
for (const searchVectorField of searchVectorFields) {
|
||||
let fieldsUsedForSearch: FieldTypeAndNameMetadata[] = [];
|
||||
let objectNameForLog = '';
|
||||
|
||||
switch (searchVectorField.standardId) {
|
||||
case NOTE_STANDARD_FIELD_IDS.searchVector: {
|
||||
fieldsUsedForSearch = SEARCH_FIELDS_FOR_NOTES;
|
||||
objectNameForLog = 'Note';
|
||||
break;
|
||||
}
|
||||
case TASK_STANDARD_FIELD_IDS.searchVector: {
|
||||
fieldsUsedForSearch = SEARCH_FIELDS_FOR_TASK;
|
||||
objectNameForLog = 'Task';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldsUsedForSearch.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Updating searchVector for ${searchVectorField.objectMetadataId} (${objectNameForLog})...`,
|
||||
);
|
||||
|
||||
await this.searchService.updateSearchVector(
|
||||
searchVectorField.objectMetadataId,
|
||||
fieldsUsedForSearch,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,75 +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 { DeleteViewFieldsWithoutViewsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-delete-view-fields-without-views.command';
|
||||
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-enforce-unique-constraints.command';
|
||||
import { SetMissingLabelIdentifierToCustomObjectsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-set-missing-label-identifier-to-custom-objects.command';
|
||||
import { UpdateRichTextSearchVectorCommand } from 'src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression';
|
||||
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';
|
||||
|
||||
interface UpdateTo0_33CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.33',
|
||||
description: 'Upgrade to 0.33',
|
||||
})
|
||||
export class UpgradeTo0_33Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly updateRichTextSearchVectorCommand: UpdateRichTextSearchVectorCommand,
|
||||
private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand,
|
||||
private readonly deleteViewFieldsWithoutViewsCommand: DeleteViewFieldsWithoutViewsCommand,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly setMissingLabelIdentifierToCustomObjectsCommand: SetMissingLabelIdentifierToCustomObjectsCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: UpdateTo0_33CommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
await this.deleteViewFieldsWithoutViewsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
await this.enforceUniqueConstraintsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
company: true,
|
||||
person: true,
|
||||
viewField: true,
|
||||
viewSort: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
force: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
await this.updateRichTextSearchVectorCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
await this.setMissingLabelIdentifierToCustomObjectsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DeleteViewFieldsWithoutViewsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-delete-view-fields-without-views.command';
|
||||
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-enforce-unique-constraints.command';
|
||||
import { SetMissingLabelIdentifierToCustomObjectsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-set-missing-label-identifier-to-custom-objects.command';
|
||||
import { UpdateRichTextSearchVectorCommand } from 'src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression';
|
||||
import { UpgradeTo0_33Command } from 'src/database/commands/upgrade-version/0-33/0-33-upgrade-version.command';
|
||||
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 { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
SearchModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeTo0_33Command,
|
||||
UpdateRichTextSearchVectorCommand,
|
||||
EnforceUniqueConstraintsCommand,
|
||||
DeleteViewFieldsWithoutViewsCommand,
|
||||
SetMissingLabelIdentifierToCustomObjectsCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_33CommandModule {}
|
||||
@ -1,143 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
// For DX only
|
||||
type WorkspaceId = string;
|
||||
|
||||
type Subdomain = string;
|
||||
|
||||
@Command({
|
||||
name: 'feat-0.34:add-subdomain-to-workspace',
|
||||
description: 'Add a default subdomain to each workspace',
|
||||
})
|
||||
export class GenerateDefaultSubdomainCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
private generatePayloadForQuery({
|
||||
id,
|
||||
subdomain,
|
||||
domainName,
|
||||
displayName,
|
||||
}: Workspace) {
|
||||
const result = { id, subdomain };
|
||||
|
||||
if (domainName) {
|
||||
const subdomain = domainName.split('.')[0];
|
||||
|
||||
if (subdomain.length > 0) {
|
||||
result.subdomain = subdomain;
|
||||
}
|
||||
}
|
||||
|
||||
if (!domainName && displayName) {
|
||||
result.subdomain = this.sanitizeForSubdomain(displayName);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private sanitizeForSubdomain(input: string) {
|
||||
const normalized = input.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
|
||||
|
||||
const hyphenated = normalized
|
||||
.replace(/[^a-zA-Z0-9]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.toLowerCase();
|
||||
|
||||
// DNS subdomain limit is 63, but we want to be conservative for duplicates
|
||||
return hyphenated.substring(0, 60);
|
||||
}
|
||||
|
||||
private groupBySubdomainName(
|
||||
acc: Record<Subdomain, Array<WorkspaceId>>,
|
||||
workspace: Workspace,
|
||||
) {
|
||||
const payload = this.generatePayloadForQuery(workspace);
|
||||
|
||||
acc[payload.subdomain] = acc[payload.subdomain]
|
||||
? acc[payload.subdomain].concat([payload.id])
|
||||
: [payload.id];
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
private async deduplicateAndSave(
|
||||
subdomain: Subdomain,
|
||||
workspaceIds: Array<WorkspaceId>,
|
||||
existingSubdomains: Set<string>,
|
||||
options: BaseCommandOptions,
|
||||
) {
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
const subdomainDeduplicated =
|
||||
index === 0
|
||||
? existingSubdomains.has(subdomain)
|
||||
? `${subdomain}-${index}`
|
||||
: subdomain
|
||||
: `${subdomain}-${index}`;
|
||||
|
||||
this.logger.log(
|
||||
`Updating workspace ${workspaceId} with subdomain ${subdomainDeduplicated}`,
|
||||
);
|
||||
|
||||
existingSubdomains.add(subdomainDeduplicated);
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
subdomain: subdomainDeduplicated,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: BaseCommandOptions,
|
||||
activeWorkspaceIds: string[],
|
||||
): Promise<void> {
|
||||
const workspaces = await this.workspaceRepository.find(
|
||||
activeWorkspaceIds.length > 0
|
||||
? {
|
||||
where: {
|
||||
id: In(activeWorkspaceIds),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
this.logger.log('No workspaces found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceBySubdomain = Object.entries(
|
||||
workspaces.reduce(
|
||||
(acc, workspace) => this.groupBySubdomainName(acc, workspace),
|
||||
{} as ReturnType<typeof this.groupBySubdomainName>,
|
||||
),
|
||||
);
|
||||
|
||||
const existingSubdomains: Set<string> = new Set();
|
||||
|
||||
for (const [subdomain, workspaceIds] of workspaceBySubdomain) {
|
||||
await this.deduplicateAndSave(
|
||||
subdomain,
|
||||
workspaceIds,
|
||||
existingSubdomains,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,38 +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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GenerateDefaultSubdomainCommand } from 'src/database/commands/upgrade-version/0-34/0-34-generate-subdomain.command';
|
||||
|
||||
interface UpdateTo0_34CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.34',
|
||||
description: 'Upgrade to 0.34',
|
||||
})
|
||||
export class UpgradeTo0_34Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly generateDefaultSubdomainCommand: GenerateDefaultSubdomainCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: UpdateTo0_34CommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
await this.generateDefaultSubdomainCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { GenerateDefaultSubdomainCommand } from 'src/database/commands/upgrade-version/0-34/0-34-generate-subdomain.command';
|
||||
import { UpgradeTo0_34Command } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.command';
|
||||
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 { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
SearchModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
],
|
||||
providers: [UpgradeTo0_34Command, GenerateDefaultSubdomainCommand],
|
||||
})
|
||||
export class UpgradeTo0_34CommandModule {}
|
||||
@ -1,144 +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 { v4 } from 'uuid';
|
||||
|
||||
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 { 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 { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.35:phone-calling-code-create-column',
|
||||
description: 'Create the callingCode column',
|
||||
})
|
||||
export class PhoneCallingCodeCreateColumnCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to add calling code and change country code with default one',
|
||||
);
|
||||
if (isCommandLogger(this.logger)) {
|
||||
this.logger.setVerbose(options.verbose ?? false);
|
||||
}
|
||||
|
||||
this.logger.verbose(`Part 1 - Workspace`);
|
||||
let workspaceIterator = 1;
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.verbose(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
|
||||
);
|
||||
|
||||
try {
|
||||
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.PHONES,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
if (
|
||||
isDefined(phoneFieldMetadata?.name) &&
|
||||
isDefined(phoneFieldMetadata.object)
|
||||
) {
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${phoneFieldMetadata.object.nameSingular || 'not found'}`,
|
||||
);
|
||||
|
||||
if (!phoneFieldMetadata.object?.nameSingular) continue;
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
continue;
|
||||
}
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`create-${phoneFieldMetadata.object.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(phoneFieldMetadata.object),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
{
|
||||
id: v4(),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
objectMetadataId: phoneFieldMetadata.object.id,
|
||||
workspaceId: workspaceId,
|
||||
isNullable: true,
|
||||
defaultValue: "''",
|
||||
} satisfies Partial<FieldMetadataEntity>,
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`,
|
||||
);
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(`Error in workspace ${workspaceId} : ${error}`);
|
||||
}
|
||||
workspaceIterator++;
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -1,306 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { getCountries, getCountryCallingCode } from 'libphonenumber-js';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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 { 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
const callingCodeToCountryCode = (callingCode: string): string => {
|
||||
if (!callingCode) {
|
||||
return '';
|
||||
}
|
||||
let callingCodeSanitized = callingCode;
|
||||
|
||||
if (callingCode.startsWith('+')) {
|
||||
callingCodeSanitized = callingCode.slice(1);
|
||||
}
|
||||
|
||||
return (
|
||||
getCountries().find(
|
||||
(countryCode) =>
|
||||
getCountryCallingCode(countryCode) === callingCodeSanitized,
|
||||
) || ''
|
||||
);
|
||||
};
|
||||
|
||||
const isCallingCode = (callingCode: string): boolean => {
|
||||
return callingCodeToCountryCode(callingCode) !== '';
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.35:phone-calling-code-migrate-data',
|
||||
description: 'Add calling code and change country code with default one',
|
||||
})
|
||||
export class PhoneCallingCodeMigrateDataCommand 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 twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to add calling code and change country code with default one',
|
||||
);
|
||||
|
||||
if (isCommandLogger(this.logger)) {
|
||||
this.logger.setVerbose(options.verbose ?? false);
|
||||
}
|
||||
this.logger.verbose(`Part 1 - Workspace`);
|
||||
|
||||
let workspaceIterator = 1;
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
|
||||
);
|
||||
|
||||
try {
|
||||
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.PHONES,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
if (
|
||||
isDefined(phoneFieldMetadata?.name) &&
|
||||
isDefined(phoneFieldMetadata.object)
|
||||
) {
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${phoneFieldMetadata.object.nameSingular || 'not found'}`,
|
||||
);
|
||||
|
||||
if (!phoneFieldMetadata.object?.nameSingular) continue;
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`create-${phoneFieldMetadata.object.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(phoneFieldMetadata.object),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
{
|
||||
id: v4(),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
objectMetadataId: phoneFieldMetadata.object.id,
|
||||
workspaceId: workspaceId,
|
||||
isNullable: true,
|
||||
defaultValue: "''",
|
||||
},
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`,
|
||||
);
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 2 - Migrations for callingCode must be first. Now can use twentyORMGlobalManager to update countryCode`,
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 3 (same time) - update CountryCode to letters: +33 => FR || +1 => US (if mulitple, first one)`,
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`P1 Step 4 (same time) - update all additioanl phones to add a country code following the same logic`,
|
||||
);
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
this.logger.verbose(`P1 Step 2 - for ${phoneFieldMetadata.name}`);
|
||||
if (
|
||||
isDefined(phoneFieldMetadata) &&
|
||||
isDefined(phoneFieldMetadata.name)
|
||||
) {
|
||||
const [objectMetadata] = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
id: phoneFieldMetadata?.objectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectMetadata.nameSingular,
|
||||
);
|
||||
const records = await repository.find();
|
||||
|
||||
for (const record of records) {
|
||||
if (
|
||||
record?.[phoneFieldMetadata.name]?.primaryPhoneCountryCode &&
|
||||
isCallingCode(
|
||||
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
|
||||
)
|
||||
) {
|
||||
let additionalPhones = null;
|
||||
|
||||
if (record[phoneFieldMetadata.name].additionalPhones) {
|
||||
additionalPhones = record[
|
||||
phoneFieldMetadata.name
|
||||
].additionalPhones.map((phone) => {
|
||||
return {
|
||||
...phone,
|
||||
countryCode: callingCodeToCountryCode(phone.callingCode),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (!options.dryRun) {
|
||||
await repository.update(record.id, {
|
||||
[`${phoneFieldMetadata.name}PrimaryPhoneCallingCode`]:
|
||||
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
|
||||
[`${phoneFieldMetadata.name}PrimaryPhoneCountryCode`]:
|
||||
callingCodeToCountryCode(
|
||||
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
|
||||
),
|
||||
[`${phoneFieldMetadata.name}AdditionalPhones`]:
|
||||
additionalPhones,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(`Error in workspace ${workspaceId} : ${error}`);
|
||||
}
|
||||
workspaceIterator++;
|
||||
}
|
||||
|
||||
this.logger.verbose(`
|
||||
|
||||
Part 2 - FieldMetadata`);
|
||||
|
||||
workspaceIterator = 1;
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`P2 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
|
||||
);
|
||||
|
||||
try {
|
||||
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.PHONES,
|
||||
},
|
||||
});
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
if (
|
||||
!isDefined(phoneFieldMetadata) ||
|
||||
!isDefined(phoneFieldMetadata.defaultValue)
|
||||
)
|
||||
continue;
|
||||
let defaultValue = phoneFieldMetadata.defaultValue;
|
||||
|
||||
// some cases look like it's an array. let's flatten it (not sure the case is supposed to happen but I saw it in my local db)
|
||||
if (Array.isArray(defaultValue) && isDefined(defaultValue[0]))
|
||||
defaultValue = phoneFieldMetadata.defaultValue[0];
|
||||
|
||||
if (!isDefined(defaultValue)) continue;
|
||||
if (typeof defaultValue !== 'object') continue;
|
||||
if (!('primaryPhoneCountryCode' in defaultValue)) continue;
|
||||
if (!defaultValue.primaryPhoneCountryCode) continue;
|
||||
|
||||
const primaryPhoneCountryCode = defaultValue.primaryPhoneCountryCode;
|
||||
|
||||
const countryCode = callingCodeToCountryCode(
|
||||
primaryPhoneCountryCode.replace(/["']/g, ''),
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
if (!defaultValue.primaryPhoneCallingCode) {
|
||||
await this.fieldMetadataRepository.update(phoneFieldMetadata.id, {
|
||||
defaultValue: {
|
||||
...defaultValue,
|
||||
primaryPhoneCountryCode: countryCode
|
||||
? `'${countryCode}'`
|
||||
: "''",
|
||||
primaryPhoneCallingCode: isCallingCode(
|
||||
primaryPhoneCountryCode.replace(/["']/g, ''),
|
||||
)
|
||||
? primaryPhoneCountryCode
|
||||
: "''",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(`Error in workspace ${workspaceId} : ${error}`);
|
||||
}
|
||||
workspaceIterator++;
|
||||
}
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -1,42 +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 { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.35:record-position-backfill',
|
||||
description: 'Backfill record position',
|
||||
})
|
||||
export class RecordPositionBackfillCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly recordPositionBackfillService: RecordPositionBackfillService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: BaseCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
await this.recordPositionBackfillService.backfill(
|
||||
workspaceId,
|
||||
options.dryRun ?? false,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error backfilling record position for workspace ${workspaceId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,74 +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 { PhoneCallingCodeCreateColumnCommand } from 'src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-create-column.command';
|
||||
import { PhoneCallingCodeMigrateDataCommand } from 'src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-migrate-data.command';
|
||||
import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-35/0-35-record-position-backfill.command';
|
||||
import { ViewGroupNoValueBackfillCommand } from 'src/database/commands/upgrade-version/0-35/0-35-view-group-no-value-backfill.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.35',
|
||||
description: 'Upgrade to 0.35',
|
||||
})
|
||||
export class UpgradeTo0_35Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly viewGroupNoValueBackfillCommand: ViewGroupNoValueBackfillCommand,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly phoneCallingCodeMigrateDataCommand: PhoneCallingCodeMigrateDataCommand,
|
||||
private readonly phoneCallingCodeCreateColumnCommand: PhoneCallingCodeCreateColumnCommand,
|
||||
private readonly recordPositionBackfillCommand: RecordPositionBackfillCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: BaseCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to upgrade to 0.35: must start with phone calling code otherwise SyncMetadata will fail',
|
||||
);
|
||||
|
||||
await this.recordPositionBackfillCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.phoneCallingCodeCreateColumnCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.phoneCallingCodeMigrateDataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.viewGroupNoValueBackfillCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
force: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { PhoneCallingCodeCreateColumnCommand } from 'src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-create-column.command';
|
||||
import { PhoneCallingCodeMigrateDataCommand } from 'src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-migrate-data.command';
|
||||
import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-35/0-35-record-position-backfill.command';
|
||||
import { UpgradeTo0_35Command } from 'src/database/commands/upgrade-version/0-35/0-35-upgrade-version.command';
|
||||
import { ViewGroupNoValueBackfillCommand } from 'src/database/commands/upgrade-version/0-35/0-35-view-group-no-value-backfill.command';
|
||||
import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
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';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
SearchModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
WorkspaceMigrationModule,
|
||||
RecordPositionBackfillModule,
|
||||
FieldMetadataModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeTo0_35Command,
|
||||
PhoneCallingCodeCreateColumnCommand,
|
||||
PhoneCallingCodeMigrateDataCommand,
|
||||
WorkspaceMigrationFactory,
|
||||
RecordPositionBackfillCommand,
|
||||
ViewGroupNoValueBackfillCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_35CommandModule {}
|
||||
@ -1,88 +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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'migrate-0.35:backfill-view-group-no-value',
|
||||
description: 'Backfill view group no value',
|
||||
})
|
||||
export class ViewGroupNoValueBackfillCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: BaseCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
const viewGroupRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewGroup',
|
||||
);
|
||||
|
||||
const views = await viewRepository.find({
|
||||
relations: ['viewGroups'],
|
||||
});
|
||||
|
||||
for (const view of views) {
|
||||
if (view.viewGroups.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're assuming for now that all viewGroups belonging to the same view have the same fieldMetadataId
|
||||
const viewGroup = view.viewGroups?.[0];
|
||||
const fieldMetadataId = viewGroup?.fieldMetadataId;
|
||||
|
||||
if (!fieldMetadataId || !viewGroup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||
where: { id: viewGroup.fieldMetadataId },
|
||||
});
|
||||
|
||||
if (!fieldMetadata) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.fieldMetadataRelatedRecordsService.syncNoValueViewGroup(
|
||||
fieldMetadata,
|
||||
view,
|
||||
viewGroupRepository,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error backfilling view group no value for workspace ${workspaceId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
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 { FeatureFlagEntity } 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,
|
||||
WorkspaceActivationStatus,
|
||||
} 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(FeatureFlagEntity, 'core')
|
||||
protected readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
@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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,25 +2,54 @@ 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 { FeatureFlagEntity } 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], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
[
|
||||
Workspace,
|
||||
BillingSubscription,
|
||||
FeatureFlagEntity,
|
||||
KeyValuePair,
|
||||
User,
|
||||
UserWorkspace,
|
||||
],
|
||||
'core',
|
||||
),
|
||||
TypeOrmModule.forFeature(
|
||||
[
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
DataSourceEntity,
|
||||
WorkspaceMigrationEntity,
|
||||
],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
TypeORMModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeTo0_40Command,
|
||||
MigrateAggregateOperationOptionsCommand,
|
||||
UpdateInactiveWorkspaceStatusCommand,
|
||||
],
|
||||
providers: [UpgradeTo0_40Command, MigrateAggregateOperationOptionsCommand],
|
||||
})
|
||||
export class UpgradeTo0_40CommandModule {}
|
||||
|
||||
@ -4,6 +4,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
@ -77,7 +78,7 @@ export class User {
|
||||
updatedAt: Date;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt: Date;
|
||||
|
||||
@OneToMany(() => AppToken, (appToken) => appToken.user, {
|
||||
|
||||
@ -4,6 +4,7 @@ import { IDField, UnPagedRelation } from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
@ -67,7 +68,7 @@ export class Workspace {
|
||||
inviteHash?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@Field()
|
||||
|
||||
Reference in New Issue
Block a user