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 { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||||
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
||||||
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
||||||
import { UpgradeTo0_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 { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
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,
|
DataSeedDemoWorkspaceModule,
|
||||||
WorkspaceCacheStorageModule,
|
WorkspaceCacheStorageModule,
|
||||||
WorkspaceMetadataVersionModule,
|
WorkspaceMetadataVersionModule,
|
||||||
UpgradeTo0_32CommandModule,
|
|
||||||
UpgradeTo0_33CommandModule,
|
|
||||||
UpgradeTo0_34CommandModule,
|
|
||||||
UpgradeTo0_35CommandModule,
|
|
||||||
UpgradeTo0_40CommandModule,
|
UpgradeTo0_40CommandModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -28,12 +28,12 @@ export class CommandLogger {
|
|||||||
this.logger.error(message, stack, context);
|
this.logger.error(message, stack, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(message: string, context?: string) {
|
warn(message: string, ...optionalParams: [...any, string?]) {
|
||||||
this.logger.warn(message, context);
|
this.logger.warn(message, ...optionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(message: string, context?: string) {
|
debug(message: string, ...optionalParams: [...any, string?]) {
|
||||||
this.logger.debug(message, context);
|
this.logger.debug(message, ...optionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
verbose(message: string, ...optionalParams: [...any, string?]) {
|
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 { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command';
|
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 { 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 { 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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
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 { 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 { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
[
|
||||||
|
Workspace,
|
||||||
|
BillingSubscription,
|
||||||
|
FeatureFlagEntity,
|
||||||
|
KeyValuePair,
|
||||||
|
User,
|
||||||
|
UserWorkspace,
|
||||||
|
],
|
||||||
|
'core',
|
||||||
|
),
|
||||||
|
TypeOrmModule.forFeature(
|
||||||
|
[
|
||||||
|
ObjectMetadataEntity,
|
||||||
|
FieldMetadataEntity,
|
||||||
|
DataSourceEntity,
|
||||||
|
WorkspaceMigrationEntity,
|
||||||
|
],
|
||||||
'metadata',
|
'metadata',
|
||||||
),
|
),
|
||||||
WorkspaceMigrationRunnerModule,
|
WorkspaceMigrationRunnerModule,
|
||||||
WorkspaceMigrationModule,
|
WorkspaceMigrationModule,
|
||||||
WorkspaceMetadataVersionModule,
|
WorkspaceMetadataVersionModule,
|
||||||
|
TypeORMModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
UpgradeTo0_40Command,
|
||||||
|
MigrateAggregateOperationOptionsCommand,
|
||||||
|
UpdateInactiveWorkspaceStatusCommand,
|
||||||
],
|
],
|
||||||
providers: [UpgradeTo0_40Command, MigrateAggregateOperationOptionsCommand],
|
|
||||||
})
|
})
|
||||||
export class UpgradeTo0_40CommandModule {}
|
export class UpgradeTo0_40CommandModule {}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
|
|||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
Index,
|
Index,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
@ -77,7 +78,7 @@ export class User {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
@Column({ nullable: true, type: 'timestamptz' })
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
deletedAt: Date;
|
deletedAt: Date;
|
||||||
|
|
||||||
@OneToMany(() => AppToken, (appToken) => appToken.user, {
|
@OneToMany(() => AppToken, (appToken) => appToken.user, {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { IDField, UnPagedRelation } from '@ptc-org/nestjs-query-graphql';
|
|||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -67,7 +68,7 @@ export class Workspace {
|
|||||||
inviteHash?: string;
|
inviteHash?: string;
|
||||||
|
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
@Column({ nullable: true, type: 'timestamptz' })
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
|
|||||||
Reference in New Issue
Block a user