6655 remove field direction in message and add it in mcma (#6743)
Closes #6655 - Remove direction from message - Add direction do mcma - Create migration command - Create upgrade 0.24
This commit is contained in:
@ -7,8 +7,7 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de
|
||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
||||
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
||||
import { UpgradeTo0_23CommandModule } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module';
|
||||
import { UpgradeVersionModule } from 'src/database/commands/upgrade-version/upgrade-version.module';
|
||||
import { UpgradeTo0_24CommandModule } from 'src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module';
|
||||
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';
|
||||
@ -46,8 +45,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
FieldMetadataModule,
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
UpgradeTo0_23CommandModule,
|
||||
UpgradeVersionModule,
|
||||
UpgradeTo0_24CommandModule,
|
||||
],
|
||||
providers: [
|
||||
DataSeedWorkspaceCommand,
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
interface BackfillNewOnboardingUserVarsCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:backfill-new-onboarding-user-vars',
|
||||
description: 'Backfill new onboarding user vars for existing workspaces',
|
||||
})
|
||||
export class BackfillNewOnboardingUserVarsCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(
|
||||
BackfillNewOnboardingUserVarsCommand.name,
|
||||
);
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: BackfillNewOnboardingUserVarsCommandOptions,
|
||||
): Promise<void> {
|
||||
const workspaces = await this.workspaceRepository.find({
|
||||
where: {
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
...(options.workspaceId && { id: options.workspaceId }),
|
||||
},
|
||||
relations: ['users'],
|
||||
});
|
||||
|
||||
if (!workspaces.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaces.length} workspaces`),
|
||||
);
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspace.id}`),
|
||||
);
|
||||
|
||||
await this.onboardingService.setOnboardingInviteTeamPending({
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
for (const user of workspace.users) {
|
||||
await this.onboardingService.setOnboardingCreateProfilePending({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -1,308 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service';
|
||||
import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { ViewService } from 'src/modules/view/services/view.service';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
|
||||
interface MigrateDomainNameFromTextToLinksCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:migrate-domain-standard-field-to-links',
|
||||
description:
|
||||
'Migrating field domainName of deprecated type TEXT to type LINKS',
|
||||
})
|
||||
export class MigrateDomainNameFromTextToLinksCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(
|
||||
MigrateDomainNameFromTextToLinksCommand.name,
|
||||
);
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceStatusService: WorkspaceStatusService,
|
||||
private readonly viewService: ViewService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description:
|
||||
'workspace id. Command runs on all active workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: MigrateDomainNameFromTextToLinksCommandOptions,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to migrate standard field domainName from text to Link',
|
||||
);
|
||||
let workspaceIds: string[] = [];
|
||||
|
||||
if (options.workspaceId) {
|
||||
workspaceIds = [options.workspaceId];
|
||||
} else {
|
||||
const activeWorkspaceIds =
|
||||
await this.workspaceStatusService.getActiveWorkspaceIds();
|
||||
|
||||
workspaceIds = activeWorkspaceIds;
|
||||
}
|
||||
|
||||
if (!workspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
try {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!dataSourceMetadata) {
|
||||
throw new Error(
|
||||
`Could not find dataSourceMetadata for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error(
|
||||
`Could not connect to dataSource for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const domainNameField = await this.fieldMetadataRepository.findOneBy({
|
||||
workspaceId,
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.domainName,
|
||||
});
|
||||
|
||||
if (!domainNameField) {
|
||||
throw new Error('Could not find domainName field');
|
||||
}
|
||||
|
||||
if (domainNameField.type === FieldMetadataType.LINKS) {
|
||||
this.logger.log(
|
||||
`Field domainName is already of type LINKS, skipping migration.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(`Attempting to migrate domainName field.`);
|
||||
|
||||
const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
|
||||
|
||||
await workspaceQueryRunner.connect();
|
||||
|
||||
const fieldName = domainNameField.name;
|
||||
const {
|
||||
id: _id,
|
||||
createdAt: _createdAt,
|
||||
updatedAt: _updatedAt,
|
||||
...domainNameFieldWithoutIdAndTimestamps
|
||||
} = domainNameField;
|
||||
|
||||
try {
|
||||
const tmpNewDomainLinksField =
|
||||
await this.fieldMetadataService.createOne({
|
||||
...domainNameFieldWithoutIdAndTimestamps,
|
||||
type: FieldMetadataType.LINKS,
|
||||
name: `${fieldName}Tmp`,
|
||||
defaultValue: {
|
||||
primaryLinkUrl: domainNameField.defaultValue,
|
||||
secondaryLinks: null,
|
||||
primaryLinkLabel: "''",
|
||||
},
|
||||
} satisfies CreateFieldInput);
|
||||
|
||||
// Migrate data from domainName to primaryLinkUrl
|
||||
await this.migrateDataWithinCompanyTable({
|
||||
sourceColumnName: `${domainNameField.name}`,
|
||||
targetColumnName: `${tmpNewDomainLinksField.name}PrimaryLinkUrl`,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
|
||||
// Duplicate initial domainName text field's views behaviour for new domainName field
|
||||
await this.viewService.removeFieldFromViews({
|
||||
workspaceId: workspaceId,
|
||||
fieldId: tmpNewDomainLinksField.id,
|
||||
});
|
||||
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
const viewFieldsWithDeprecatedField = await viewFieldRepository.find({
|
||||
where: {
|
||||
fieldMetadataId: domainNameField.id,
|
||||
isVisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.viewService.addFieldToViews({
|
||||
workspaceId: workspaceId,
|
||||
fieldId: tmpNewDomainLinksField.id,
|
||||
viewsIds: viewFieldsWithDeprecatedField
|
||||
.filter((viewField) => viewField.viewId !== null)
|
||||
.map((viewField) => viewField.viewId as string),
|
||||
positions: viewFieldsWithDeprecatedField.reduce(
|
||||
(acc, viewField) => {
|
||||
if (!viewField.viewId) {
|
||||
return acc;
|
||||
}
|
||||
acc[viewField.viewId] = viewField.position;
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
),
|
||||
size: 150,
|
||||
});
|
||||
|
||||
// Delete initial domainName text field
|
||||
await this.fieldMetadataService.deleteOneField(
|
||||
{ id: domainNameField.id },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
// Rename temporary domainName links field
|
||||
await this.fieldMetadataService.updateOne(tmpNewDomainLinksField.id, {
|
||||
id: tmpNewDomainLinksField.id,
|
||||
workspaceId: tmpNewDomainLinksField.workspaceId,
|
||||
name: `${fieldName}`,
|
||||
isCustom: false,
|
||||
});
|
||||
|
||||
this.logger.log(`Migration of domainName done!`);
|
||||
} catch (error) {
|
||||
this.logger.log(`Error: ${error.message}`);
|
||||
this.logger.log(
|
||||
`Failed to migrate domainName ${domainNameField.id}, rolling back.`,
|
||||
);
|
||||
|
||||
// Re-create initial field if it was deleted
|
||||
const initialField =
|
||||
await this.fieldMetadataService.findOneWithinWorkspace(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
name: `${domainNameField.name}`,
|
||||
objectMetadataId: domainNameField.objectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const tmpNewDomainLinksField =
|
||||
await this.fieldMetadataService.findOneWithinWorkspace(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
name: `${domainNameField.name}Tmp`,
|
||||
objectMetadataId: domainNameField.objectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!initialField) {
|
||||
this.logger.log(`Re-creating initial domainName field`);
|
||||
const restoredField = await this.fieldMetadataService.createOne({
|
||||
...domainNameField,
|
||||
});
|
||||
|
||||
if (tmpNewDomainLinksField) {
|
||||
this.logger.log(`Restoring data in domainName`);
|
||||
await this.migrateDataWithinCompanyTable({
|
||||
sourceColumnName: `${tmpNewDomainLinksField.name}PrimaryLinkLabel`,
|
||||
targetColumnName: `${restoredField.name}PrimaryLinkLabel`,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
|
||||
await this.migrateDataWithinCompanyTable({
|
||||
sourceColumnName: `${tmpNewDomainLinksField.name}PrimaryLinkUrl`,
|
||||
targetColumnName: `${restoredField.name}PrimaryLinkUrl`,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Failed to restore data in domainName field ${domainNameField.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (tmpNewDomainLinksField) {
|
||||
await this.fieldMetadataService.deleteOneField(
|
||||
{ id: tmpNewDomainLinksField.id },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await workspaceQueryRunner.release();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateDataWithinCompanyTable({
|
||||
sourceColumnName,
|
||||
targetColumnName,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
}: {
|
||||
sourceColumnName: string;
|
||||
targetColumnName: string;
|
||||
workspaceQueryRunner: QueryRunner;
|
||||
dataSourceMetadata: DataSourceEntity;
|
||||
}) {
|
||||
await workspaceQueryRunner.query(
|
||||
`UPDATE "${dataSourceMetadata.schema}"."company" SET "${targetColumnName}" = CASE WHEN "${sourceColumnName}" IS NULL OR "${sourceColumnName}" = '' THEN "${sourceColumnName}" WHEN "${sourceColumnName}" LIKE 'http%' THEN "${sourceColumnName}" ELSE 'https://' || "${sourceColumnName}" END;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,342 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataDefaultValueLink } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service';
|
||||
import { ViewService } from 'src/modules/view/services/view.service';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
|
||||
interface MigrateLinkFieldsToLinksCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:migrate-link-fields-to-links',
|
||||
description: 'Migrating fields of deprecated type LINK to type LINKS',
|
||||
})
|
||||
export class MigrateLinkFieldsToLinksCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(MigrateLinkFieldsToLinksCommand.name);
|
||||
constructor(
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceStatusService: WorkspaceStatusService,
|
||||
private readonly viewService: ViewService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description:
|
||||
'workspace id. Command runs on all active workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: MigrateLinkFieldsToLinksCommandOptions,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to migrate link type fields to links type',
|
||||
);
|
||||
let workspaceIds: string[] = [];
|
||||
|
||||
if (options.workspaceId) {
|
||||
workspaceIds = [options.workspaceId];
|
||||
} else {
|
||||
const activeWorkspaceIds =
|
||||
await this.workspaceStatusService.getActiveWorkspaceIds();
|
||||
|
||||
workspaceIds = activeWorkspaceIds;
|
||||
}
|
||||
|
||||
if (!workspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
||||
try {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!dataSourceMetadata) {
|
||||
throw new Error(
|
||||
`Could not find dataSourceMetadata for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error(
|
||||
`Could not connect to dataSource for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldsWithLinkType = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.LINK,
|
||||
},
|
||||
});
|
||||
|
||||
for (const fieldWithLinkType of fieldsWithLinkType) {
|
||||
const objectMetadata = await this.objectMetadataRepository.findOne({
|
||||
where: { id: fieldWithLinkType.objectMetadataId },
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new Error(
|
||||
`Could not find objectMetadata for field ${fieldWithLinkType.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Attempting to migrate field ${fieldWithLinkType.name} on ${objectMetadata.nameSingular}.`,
|
||||
);
|
||||
const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
|
||||
|
||||
await workspaceQueryRunner.connect();
|
||||
|
||||
const fieldName = fieldWithLinkType.name;
|
||||
const { id: _id, ...fieldWithLinkTypeWithoutId } = fieldWithLinkType;
|
||||
|
||||
const linkDefaultValue =
|
||||
fieldWithLinkTypeWithoutId.defaultValue as FieldMetadataDefaultValueLink;
|
||||
|
||||
const defaultValueForLinksField = {
|
||||
primaryLinkUrl: linkDefaultValue.url,
|
||||
primaryLinkLabel: linkDefaultValue.label,
|
||||
secondaryLinks: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const tmpNewLinksField = await this.fieldMetadataService.createOne({
|
||||
...fieldWithLinkTypeWithoutId,
|
||||
type: FieldMetadataType.LINKS,
|
||||
defaultValue: defaultValueForLinksField,
|
||||
name: `${fieldName}Tmp`,
|
||||
} satisfies CreateFieldInput);
|
||||
|
||||
const tableName = computeTableName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
);
|
||||
|
||||
// Migrate data from linkLabel to primaryLinkLabel
|
||||
await this.migrateDataWithinTable({
|
||||
sourceColumnName: `${fieldWithLinkType.name}Label`,
|
||||
targetColumnName: `${tmpNewLinksField.name}PrimaryLinkLabel`,
|
||||
tableName,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
|
||||
// Migrate data from linkUrl to primaryLinkUrl
|
||||
await this.migrateDataWithinTable({
|
||||
sourceColumnName: `${fieldWithLinkType.name}Url`,
|
||||
targetColumnName: `${tmpNewLinksField.name}PrimaryLinkUrl`,
|
||||
tableName,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
|
||||
// Duplicate link field's views behaviour for new links field
|
||||
await this.viewService.removeFieldFromViews({
|
||||
workspaceId: workspaceId,
|
||||
fieldId: tmpNewLinksField.id,
|
||||
});
|
||||
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
const viewFieldsWithDeprecatedField =
|
||||
await viewFieldRepository.find({
|
||||
where: {
|
||||
fieldMetadataId: fieldWithLinkType.id,
|
||||
isVisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.viewService.addFieldToViews({
|
||||
workspaceId: workspaceId,
|
||||
fieldId: tmpNewLinksField.id,
|
||||
viewsIds: viewFieldsWithDeprecatedField
|
||||
.filter((viewField) => viewField.viewId !== null)
|
||||
.map((viewField) => viewField.viewId as string),
|
||||
positions: viewFieldsWithDeprecatedField.reduce(
|
||||
(acc, viewField) => {
|
||||
if (!viewField.viewId) {
|
||||
return acc;
|
||||
}
|
||||
acc[viewField.viewId] = viewField.position;
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
),
|
||||
});
|
||||
|
||||
// Delete link field
|
||||
await this.fieldMetadataService.deleteOneField(
|
||||
{ id: fieldWithLinkType.id },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
// Rename temporary links field
|
||||
await this.fieldMetadataService.updateOne(tmpNewLinksField.id, {
|
||||
id: tmpNewLinksField.id,
|
||||
workspaceId: tmpNewLinksField.workspaceId,
|
||||
name: `${fieldName}`,
|
||||
isCustom: false,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Migration of ${fieldWithLinkType.name} on ${objectMetadata.nameSingular} done!`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
`Failed to migrate field ${fieldWithLinkType.name} on ${objectMetadata.nameSingular}, rolling back.`,
|
||||
);
|
||||
|
||||
// Re-create initial field if it was deleted
|
||||
const initialField =
|
||||
await this.fieldMetadataService.findOneWithinWorkspace(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
name: `${fieldWithLinkType.name}`,
|
||||
objectMetadataId: fieldWithLinkType.objectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const tmpNewLinksField =
|
||||
await this.fieldMetadataService.findOneWithinWorkspace(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
name: `${fieldWithLinkType.name}Tmp`,
|
||||
objectMetadataId: fieldWithLinkType.objectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!initialField) {
|
||||
this.logger.log(
|
||||
`Re-creating initial link field ${fieldWithLinkType.name} but of type links`, // Cannot create link fields anymore
|
||||
);
|
||||
const restoredField = await this.fieldMetadataService.createOne({
|
||||
...fieldWithLinkType,
|
||||
defaultValue: defaultValueForLinksField,
|
||||
type: FieldMetadataType.LINKS,
|
||||
});
|
||||
const tableName = computeTableName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
);
|
||||
|
||||
if (tmpNewLinksField) {
|
||||
this.logger.log(
|
||||
`Restoring data in field ${fieldWithLinkType.name}`,
|
||||
);
|
||||
await this.migrateDataWithinTable({
|
||||
sourceColumnName: `${tmpNewLinksField.name}PrimaryLinkLabel`,
|
||||
targetColumnName: `${restoredField.name}PrimaryLinkLabel`,
|
||||
tableName,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
|
||||
await this.migrateDataWithinTable({
|
||||
sourceColumnName: `${tmpNewLinksField.name}PrimaryLinkUrl`,
|
||||
targetColumnName: `${restoredField.name}PrimaryLinkUrl`,
|
||||
tableName,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
});
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Failed to restore data in link field ${fieldWithLinkType.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (tmpNewLinksField) {
|
||||
await this.fieldMetadataService.deleteOneField(
|
||||
{ id: tmpNewLinksField.id },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await workspaceQueryRunner.release();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
`Running command on workspace ${workspaceId} failed with error: ${error}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateDataWithinTable({
|
||||
sourceColumnName,
|
||||
targetColumnName,
|
||||
tableName,
|
||||
workspaceQueryRunner,
|
||||
dataSourceMetadata,
|
||||
}: {
|
||||
sourceColumnName: string;
|
||||
targetColumnName: string;
|
||||
tableName: string;
|
||||
workspaceQueryRunner: QueryRunner;
|
||||
dataSourceMetadata: DataSourceEntity;
|
||||
}) {
|
||||
await workspaceQueryRunner.query(
|
||||
`UPDATE "${dataSourceMetadata.schema}"."${tableName}" SET "${targetColumnName}" = "${sourceColumnName}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,231 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
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/workspace-metadata-version.service';
|
||||
import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service';
|
||||
import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
interface MigrateMessageChannelSyncStatusEnumCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:update-message-channel-sync-status-enum',
|
||||
description: 'Migrate messageChannel syncStatus enum',
|
||||
})
|
||||
export class MigrateMessageChannelSyncStatusEnumCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(
|
||||
MigrateMessageChannelSyncStatusEnumCommand.name,
|
||||
);
|
||||
constructor(
|
||||
private readonly workspaceStatusService: WorkspaceStatusService,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: MigrateMessageChannelSyncStatusEnumCommandOptions,
|
||||
): Promise<void> {
|
||||
let workspaceIds: string[] = [];
|
||||
|
||||
if (options.workspaceId) {
|
||||
workspaceIds = [options.workspaceId];
|
||||
} else {
|
||||
workspaceIds = await this.workspaceStatusService.getActiveWorkspaceIds();
|
||||
}
|
||||
|
||||
if (!workspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
const dataSourceMetadatas =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const dataSourceMetadata of dataSourceMetadatas) {
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (workspaceDataSource) {
|
||||
const queryRunner = workspaceDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum" RENAME TO "messageChannel_syncStatus_enum_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum" AS ENUM (
|
||||
'ONGOING',
|
||||
'NOT_SYNCED',
|
||||
'ACTIVE',
|
||||
'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||
'FAILED_UNKNOWN'
|
||||
)`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" TYPE text`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`UPDATE "${dataSourceMetadata.schema}"."messageChannel" SET "syncStatus" = 'ACTIVE' WHERE "syncStatus" = 'COMPLETED'`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum" USING "syncStatus"::text::"${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${dataSourceMetadata.schema}"."messageChannel" ALTER COLUMN "syncStatus" SET DEFAULT NULL`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "${dataSourceMetadata.schema}"."messageChannel_syncStatus_enum_old"`,
|
||||
);
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
chalk.red(`Running command on workspace ${workspaceId} failed`),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageChannelObjectMetadata =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: { nameSingular: 'messageChannel', workspaceId },
|
||||
});
|
||||
|
||||
if (!messageChannelObjectMetadata) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Object metadata for messageChannel not found in workspace ${workspaceId}`,
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const syncStatusFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
name: 'syncStatus',
|
||||
workspaceId,
|
||||
objectMetadataId: messageChannelObjectMetadata.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!syncStatusFieldMetadata) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`Field metadata for syncStatus not found in workspace ${workspaceId}`,
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const newOptions = [
|
||||
{
|
||||
id: v4(),
|
||||
value: MessageChannelSyncStatus.ONGOING,
|
||||
label: 'Ongoing',
|
||||
position: 1,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
value: MessageChannelSyncStatus.NOT_SYNCED,
|
||||
label: 'Not Synced',
|
||||
position: 2,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
value: MessageChannelSyncStatus.ACTIVE,
|
||||
label: 'Active',
|
||||
position: 3,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
value: MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
label: 'Failed Insufficient Permissions',
|
||||
position: 4,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
value: MessageChannelSyncStatus.FAILED_UNKNOWN,
|
||||
label: 'Failed Unknown',
|
||||
position: 5,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
|
||||
await this.fieldMetadataRepository.update(syncStatusFieldMetadata.id, {
|
||||
options: newOptions,
|
||||
});
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Migration failed for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
KeyValuePair,
|
||||
KeyValuePairType,
|
||||
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { CalendarChannelSyncStatus } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
|
||||
import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
interface SetUserVarsAccountsToReconnectCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:set-user-vars-accounts-to-reconnect',
|
||||
description: 'Set user vars accounts to reconnect',
|
||||
})
|
||||
export class SetUserVarsAccountsToReconnectCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(
|
||||
SetUserVarsAccountsToReconnectCommand.name,
|
||||
);
|
||||
constructor(
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||
@InjectRepository(KeyValuePair, 'core')
|
||||
private readonly keyValuePairRepository: Repository<KeyValuePair>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: SetUserVarsAccountsToReconnectCommandOptions,
|
||||
): Promise<void> {
|
||||
let activeWorkspaceIds: string[] = [];
|
||||
|
||||
if (options.workspaceId) {
|
||||
activeWorkspaceIds = [options.workspaceId];
|
||||
} else {
|
||||
const activeWorkspaces = await this.workspaceRepository.find({
|
||||
where: {
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
...(options.workspaceId && { id: options.workspaceId }),
|
||||
},
|
||||
});
|
||||
|
||||
activeWorkspaceIds = activeWorkspaces.map((workspace) => workspace.id);
|
||||
}
|
||||
|
||||
if (!activeWorkspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Running command on ${activeWorkspaceIds.length} workspaces`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove all deprecated user vars
|
||||
await this.keyValuePairRepository.delete({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
key: 'ACCOUNTS_TO_RECONNECT',
|
||||
});
|
||||
|
||||
for (const workspaceId of activeWorkspaceIds) {
|
||||
try {
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
try {
|
||||
const connectedAccountsInFailedInsufficientPermissions =
|
||||
await connectedAccountRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
accountOwner: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
where: [
|
||||
{
|
||||
messageChannels: {
|
||||
syncStatus:
|
||||
MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
},
|
||||
},
|
||||
{
|
||||
calendarChannels: {
|
||||
syncStatus:
|
||||
CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
},
|
||||
},
|
||||
],
|
||||
relations: {
|
||||
accountOwner: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const connectedAccount of connectedAccountsInFailedInsufficientPermissions) {
|
||||
try {
|
||||
await this.accountsToReconnectService.addAccountToReconnectByKey(
|
||||
AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
|
||||
connectedAccount.accountOwner.userId,
|
||||
workspaceId,
|
||||
connectedAccount.id,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to add account to reconnect for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(`Running command on workspace ${workspaceId} failed`),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Migration failed for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
|
||||
interface SetWorkspaceActivationStatusCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:set-workspace-activation-status',
|
||||
description: 'Set workspace activation status',
|
||||
})
|
||||
export class SetWorkspaceActivationStatusCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(
|
||||
SetWorkspaceActivationStatusCommand.name,
|
||||
);
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: SetWorkspaceActivationStatusCommandOptions,
|
||||
): Promise<void> {
|
||||
let activeSubscriptionWorkspaceIds: string[] = [];
|
||||
|
||||
if (options.workspaceId) {
|
||||
activeSubscriptionWorkspaceIds = [options.workspaceId];
|
||||
} else {
|
||||
activeSubscriptionWorkspaceIds =
|
||||
await this.billingSubscriptionService.getActiveSubscriptionWorkspaceIds();
|
||||
}
|
||||
|
||||
if (!activeSubscriptionWorkspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Running command on ${activeSubscriptionWorkspaceIds.length} workspaces`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceId of activeSubscriptionWorkspaceIds) {
|
||||
try {
|
||||
const dataSourceMetadatas =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const dataSourceMetadata of dataSourceMetadatas) {
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (workspaceDataSource) {
|
||||
try {
|
||||
await this.workspaceRepository.update(
|
||||
{ id: workspaceId },
|
||||
{ activationStatus: WorkspaceActivationStatus.ACTIVE },
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(`Running command on workspace ${workspaceId} failed`),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Migration failed for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -1,494 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { QueryRunner } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { notesAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view';
|
||||
import { tasksAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view';
|
||||
import { tasksByStatusView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view';
|
||||
import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service';
|
||||
import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/activity.workspace-entity';
|
||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity';
|
||||
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
|
||||
interface UpdateActivitiesCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
type CoreLogicFunction = (params: {
|
||||
workspaceId: string;
|
||||
queryRunner?: QueryRunner;
|
||||
schema?: string;
|
||||
}) => Promise<void>;
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23:update-activities-type',
|
||||
description: 'Migrate Activity object to Note and Task objects',
|
||||
})
|
||||
export class UpdateActivitiesCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(UpdateActivitiesCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceStatusService: WorkspaceStatusService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: UpdateActivitiesCommandOptions,
|
||||
): Promise<void> {
|
||||
const updateActivities = async ({
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
schema,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
queryRunner: QueryRunner;
|
||||
schema: string;
|
||||
}): Promise<void> => {
|
||||
/***********************
|
||||
// Transfer Activities to NOTE + Tasks
|
||||
***********************/
|
||||
|
||||
const activityRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ActivityWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'activity',
|
||||
);
|
||||
const noteRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<NoteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'note',
|
||||
);
|
||||
const noteTargetRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<NoteTargetWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'noteTarget',
|
||||
);
|
||||
const taskRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<TaskWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'task',
|
||||
);
|
||||
const taskTargetRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<TaskTargetWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'taskTarget',
|
||||
);
|
||||
const timelineActivityRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<TimelineActivityWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'timelineActivity',
|
||||
);
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
const noteObjectMetadataId = objectMetadata.find(
|
||||
(object) => object.nameSingular === 'note',
|
||||
)?.id;
|
||||
|
||||
const taskObjectMetadataId = objectMetadata.find(
|
||||
(object) => object.nameSingular === 'task',
|
||||
)?.id;
|
||||
|
||||
const activityObjectMetadataId = objectMetadata.find(
|
||||
(object) => object.nameSingular === 'activity',
|
||||
)?.id;
|
||||
|
||||
const activitiesToTransfer = await activityRepository.find({
|
||||
order: { createdAt: 'ASC' },
|
||||
relations: ['activityTargets'],
|
||||
});
|
||||
|
||||
for (let i = 0; i < activitiesToTransfer.length; i++) {
|
||||
const activity = activitiesToTransfer[i];
|
||||
|
||||
if (activity.type === 'Note') {
|
||||
const note = noteRepository.create({
|
||||
id: activity.id,
|
||||
title: activity.title,
|
||||
body: activity.body,
|
||||
createdAt: activity.createdAt,
|
||||
updatedAt: activity.updatedAt,
|
||||
position: i,
|
||||
});
|
||||
|
||||
await noteRepository.save(note);
|
||||
|
||||
if (activity.activityTargets && activity.activityTargets.length > 0) {
|
||||
const noteTargets = activity.activityTargets.map(
|
||||
(activityTarget) => {
|
||||
const { activityId, ...activityTargetData } = activityTarget;
|
||||
|
||||
return noteTargetRepository.create({
|
||||
noteId: activityId,
|
||||
...activityTargetData,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await noteTargetRepository.save(noteTargets);
|
||||
}
|
||||
|
||||
await timelineActivityRepository.update(
|
||||
{
|
||||
name: 'note.created',
|
||||
linkedObjectMetadataId: activityObjectMetadataId,
|
||||
linkedRecordId: activity.id,
|
||||
},
|
||||
{
|
||||
linkedObjectMetadataId: noteObjectMetadataId,
|
||||
name: 'linked-note.created',
|
||||
},
|
||||
);
|
||||
|
||||
await timelineActivityRepository.update(
|
||||
{
|
||||
name: 'note.updated',
|
||||
linkedObjectMetadataId: activityObjectMetadataId,
|
||||
linkedRecordId: activity.id,
|
||||
},
|
||||
{
|
||||
linkedObjectMetadataId: noteObjectMetadataId,
|
||||
name: 'linked-note.updated',
|
||||
},
|
||||
);
|
||||
|
||||
await attachmentRepository.update(
|
||||
{
|
||||
activityId: activity.id,
|
||||
},
|
||||
{
|
||||
activityId: null,
|
||||
noteId: activity.id,
|
||||
},
|
||||
);
|
||||
} else if (activity.type === 'Task') {
|
||||
const task = taskRepository.create({
|
||||
id: activity.id,
|
||||
title: activity.title,
|
||||
body: activity.body,
|
||||
status: activity.completedAt ? 'DONE' : 'TODO',
|
||||
dueAt: activity.dueAt,
|
||||
assigneeId: activity.assigneeId,
|
||||
position: i,
|
||||
createdAt: activity.createdAt,
|
||||
updatedAt: activity.updatedAt,
|
||||
});
|
||||
|
||||
await taskRepository.save(task);
|
||||
|
||||
if (activity.activityTargets && activity.activityTargets.length > 0) {
|
||||
const taskTargets = activity.activityTargets.map(
|
||||
(activityTarget) => {
|
||||
const { activityId, ...activityTargetData } = activityTarget;
|
||||
|
||||
return taskTargetRepository.create({
|
||||
taskId: activityId,
|
||||
...activityTargetData,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await taskTargetRepository.save(taskTargets);
|
||||
}
|
||||
|
||||
await timelineActivityRepository.update(
|
||||
{
|
||||
name: 'task.created',
|
||||
linkedObjectMetadataId: activityObjectMetadataId,
|
||||
linkedRecordId: activity.id,
|
||||
},
|
||||
{
|
||||
linkedObjectMetadataId: taskObjectMetadataId,
|
||||
name: 'linked-task.created',
|
||||
},
|
||||
);
|
||||
|
||||
await timelineActivityRepository.update(
|
||||
{
|
||||
name: 'task.updated',
|
||||
linkedObjectMetadataId: activityObjectMetadataId,
|
||||
linkedRecordId: activity.id,
|
||||
},
|
||||
{
|
||||
linkedObjectMetadataId: taskObjectMetadataId,
|
||||
name: 'linked-task.updated',
|
||||
},
|
||||
);
|
||||
await attachmentRepository.update(
|
||||
{
|
||||
activityId: activity.id,
|
||||
},
|
||||
{
|
||||
activityId: null,
|
||||
taskId: activity.id,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unknown activity type: ${activity.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to make sure the command is indempotent and return if one of the view exists
|
||||
const viewExists = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.from(`${schema}.view`, 'view')
|
||||
.where('name = :name', { name: 'All Notes' })
|
||||
.getRawOne();
|
||||
|
||||
if (!viewExists) {
|
||||
await this.createViews(
|
||||
objectMetadata,
|
||||
queryRunner,
|
||||
schema,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return this.sharedBoilerplate(_passedParam, options, updateActivities);
|
||||
}
|
||||
|
||||
private async createViews(
|
||||
objectMetadata: ObjectMetadataEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
schema: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
|
||||
acc[object.standardId ?? ''] = {
|
||||
id: object.id,
|
||||
fields: object.fields.reduce((acc, field) => {
|
||||
acc[field.standardId ?? ''] = field.id;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {}) as Record<string, ObjectMetadataEntity>;
|
||||
|
||||
const viewDefinitions = [
|
||||
await notesAllView(objectMetadataMap),
|
||||
await tasksAllView(objectMetadataMap),
|
||||
await tasksByStatusView(objectMetadataMap),
|
||||
];
|
||||
|
||||
const viewDefinitionsWithId = viewDefinitions.map((viewDefinition) => ({
|
||||
...viewDefinition,
|
||||
id: v4(),
|
||||
}));
|
||||
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schema}.view`, [
|
||||
'id',
|
||||
'name',
|
||||
'objectMetadataId',
|
||||
'type',
|
||||
'key',
|
||||
'position',
|
||||
'icon',
|
||||
'kanbanFieldMetadataId',
|
||||
])
|
||||
.values(
|
||||
viewDefinitionsWithId.map(
|
||||
({
|
||||
id,
|
||||
name,
|
||||
objectMetadataId,
|
||||
type,
|
||||
key,
|
||||
position,
|
||||
icon,
|
||||
kanbanFieldMetadataId,
|
||||
}) => ({
|
||||
id,
|
||||
name,
|
||||
objectMetadataId,
|
||||
type,
|
||||
key,
|
||||
position,
|
||||
icon,
|
||||
kanbanFieldMetadataId,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
for (const viewDefinition of viewDefinitionsWithId) {
|
||||
if (viewDefinition.fields && viewDefinition.fields.length > 0) {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schema}.viewField`, [
|
||||
'fieldMetadataId',
|
||||
'position',
|
||||
'isVisible',
|
||||
'size',
|
||||
'viewId',
|
||||
])
|
||||
.values(
|
||||
viewDefinition.fields.map((field) => ({
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
position: field.position,
|
||||
isVisible: field.isVisible,
|
||||
size: field.size,
|
||||
viewId: viewDefinition.id,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
if (viewDefinition.filters && viewDefinition.filters.length > 0) {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schema}.viewFilter`, [
|
||||
'fieldMetadataId',
|
||||
'displayValue',
|
||||
'operand',
|
||||
'value',
|
||||
'viewId',
|
||||
])
|
||||
.values(
|
||||
viewDefinition.filters.map((filter: any) => ({
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
displayValue: filter.displayValue,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewId: viewDefinition.id,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This is an attempt to do something more generic that could be reused in every command
|
||||
// Next step if it works well for a few command is to isolated it into a file so
|
||||
// it can be reused and not copy-pasted.
|
||||
async sharedBoilerplate(
|
||||
_passedParam: string[],
|
||||
options: UpdateActivitiesCommandOptions,
|
||||
coreLogic: CoreLogicFunction,
|
||||
) {
|
||||
const workspaceIds = options.workspaceId
|
||||
? [options.workspaceId]
|
||||
: await this.workspaceStatusService.getActiveWorkspaceIds();
|
||||
|
||||
if (!workspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
|
||||
const requiresQueryRunner =
|
||||
coreLogic.toString().includes('queryRunner') ||
|
||||
coreLogic.toString().includes('schema');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
if (requiresQueryRunner) {
|
||||
await this.executeWithQueryRunner(workspaceId, coreLogic);
|
||||
} else {
|
||||
await coreLogic({ workspaceId });
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Migration failed for workspace ${workspaceId}: ${error.message}, ${error.stack}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async executeWithQueryRunner(
|
||||
workspaceId: string,
|
||||
coreLogic: CoreLogicFunction,
|
||||
) {
|
||||
const dataSourceMetadatas =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const dataSourceMetadata of dataSourceMetadatas) {
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (workspaceDataSource) {
|
||||
const queryRunner = workspaceDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await coreLogic({
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
schema: dataSourceMetadata.schema,
|
||||
});
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
chalk.red(`Running command on workspace ${workspaceId} failed`),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,235 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import pLimit from 'p-limit';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
|
||||
interface UpdateFileFolderStructureCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0-23:update-file-folder-structure',
|
||||
description: 'Update file folder structure (prefixed per workspace)',
|
||||
})
|
||||
export class UpdateFileFolderStructureCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(UpdateFileFolderStructureCommand.name);
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: UpdateFileFolderStructureCommandOptions,
|
||||
): Promise<void> {
|
||||
const workspaceIds = options.workspaceId
|
||||
? [options.workspaceId]
|
||||
: (
|
||||
await this.workspaceRepository.find({
|
||||
where: { activationStatus: WorkspaceActivationStatus.ACTIVE },
|
||||
})
|
||||
).map((workspace) => workspace.id);
|
||||
|
||||
if (!workspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!dataSourceMetadata) {
|
||||
this.logger.log(
|
||||
`Could not find dataSourceMetadata for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error(
|
||||
`Could not connect to dataSource for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
|
||||
|
||||
const attachmentsToMove = (await workspaceQueryRunner.query(
|
||||
`SELECT id, "fullPath" FROM "${dataSourceMetadata.schema}"."attachment" WHERE "fullPath" LIKE '${FileFolder.Attachment}/%'`,
|
||||
)) as { id: string; fullPath: string }[];
|
||||
|
||||
const workspaceMemberAvatarsToMove = (await workspaceQueryRunner.query(
|
||||
`SELECT id, "avatarUrl" as "fullPath" FROM "${dataSourceMetadata.schema}"."workspaceMember" WHERE "avatarUrl" LIKE '${FileFolder.ProfilePicture}/%'`,
|
||||
)) as { id: string; fullPath: string }[];
|
||||
|
||||
const personAvatarsToMove = (await workspaceQueryRunner.query(
|
||||
`SELECT id, "avatarUrl" as "fullPath" FROM "${dataSourceMetadata.schema}"."person" WHERE "avatarUrl" LIKE '${FileFolder.PersonPicture}/%'`,
|
||||
)) as { id: string; fullPath: string }[];
|
||||
|
||||
const workspacePictureToMove = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
logo: Like(`${FileFolder.WorkspaceLogo}/%`),
|
||||
});
|
||||
|
||||
try {
|
||||
const updatedAttachments = await this.moveFiles(
|
||||
workspaceId,
|
||||
attachmentsToMove,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Moved ${updatedAttachments.length} attachments in workspace ${workspaceId}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedWorkspaceMemberAvatars = await this.moveFiles(
|
||||
workspaceId,
|
||||
workspaceMemberAvatarsToMove,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Moved ${updatedWorkspaceMemberAvatars.length} workspaceMemberAvatars in workspace ${workspaceId}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedPersonAvatars = await this.moveFiles(
|
||||
workspaceId,
|
||||
personAvatarsToMove,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Moved ${updatedPersonAvatars.length} personAvatars in workspace ${workspaceId}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
}
|
||||
|
||||
if (workspacePictureToMove?.logo) {
|
||||
await this.moveFiles(workspaceId, [
|
||||
{
|
||||
id: workspacePictureToMove.id,
|
||||
fullPath: workspacePictureToMove.logo,
|
||||
},
|
||||
]);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Moved workspacePicture in workspace ${workspaceId}`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async moveFiles(
|
||||
workspaceId: string,
|
||||
filesToMove: { id: string; fullPath: string }[],
|
||||
): Promise<Array<{ id: string; updatedFolderPath: string }>> {
|
||||
const batchSize = 20;
|
||||
const limit = pLimit(batchSize);
|
||||
|
||||
const moveFile = async ({
|
||||
id,
|
||||
fullPath,
|
||||
}: {
|
||||
id: string;
|
||||
fullPath: string;
|
||||
}) => {
|
||||
const pathParts = fullPath.split('/');
|
||||
const filename = pathParts.pop();
|
||||
|
||||
if (!filename) {
|
||||
throw new Error(`Filename is empty for file ID: ${id}`);
|
||||
}
|
||||
|
||||
const originalFolderPath = pathParts.join('/');
|
||||
const updatedFolderPath = `workspace-${workspaceId}/${originalFolderPath}`;
|
||||
|
||||
try {
|
||||
await this.fileStorageService.move({
|
||||
from: { folderPath: originalFolderPath, filename },
|
||||
to: { folderPath: updatedFolderPath, filename },
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
this.logger.error(`File not found: ${fullPath}`);
|
||||
} else {
|
||||
this.logger.error(`Error moving file ${fullPath}: ${error}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return { id, updatedFolderPath };
|
||||
};
|
||||
|
||||
const movePromises = filesToMove.map((file) => limit(() => moveFile(file)));
|
||||
|
||||
const results = await Promise.all(movePromises);
|
||||
|
||||
return results.filter(
|
||||
(result): result is { id: string; updatedFolderPath: string } =>
|
||||
Boolean(result),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { BackfillNewOnboardingUserVarsCommand } from 'src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars';
|
||||
import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command';
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetUserVarsAccountsToReconnectCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
import { UpdateFileFolderStructureCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
interface UpdateTo0_23CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.23',
|
||||
description: 'Upgrade to 0.23',
|
||||
})
|
||||
export class UpgradeTo0_23Command extends CommandRunner {
|
||||
constructor(
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly updateFileFolderStructureCommandOptions: UpdateFileFolderStructureCommand,
|
||||
private readonly migrateLinkFieldsToLinks: MigrateLinkFieldsToLinksCommand,
|
||||
private readonly migrateDomainNameFromTextToLinks: MigrateDomainNameFromTextToLinksCommand,
|
||||
private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand,
|
||||
private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand,
|
||||
private readonly updateActivitiesCommand: UpdateActivitiesCommand,
|
||||
private readonly backfillNewOnboardingUserVarsCommand: BackfillNewOnboardingUserVarsCommand,
|
||||
private readonly setUserVarsAccountsToReconnectCommand: SetUserVarsAccountsToReconnectCommand,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description:
|
||||
'workspace id. Command runs on all active workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: UpdateTo0_23CommandOptions,
|
||||
): Promise<void> {
|
||||
await this.migrateLinkFieldsToLinks.run(_passedParam, options);
|
||||
await this.migrateDomainNameFromTextToLinks.run(_passedParam, options);
|
||||
await this.migrateMessageChannelSyncStatusEnumCommand.run(
|
||||
_passedParam,
|
||||
options,
|
||||
);
|
||||
await this.setWorkspaceActivationStatusCommand.run(_passedParam, options);
|
||||
await this.updateFileFolderStructureCommandOptions.run(
|
||||
_passedParam,
|
||||
options,
|
||||
);
|
||||
await this.syncWorkspaceMetadataCommand.run(_passedParam, {
|
||||
...options,
|
||||
force: true,
|
||||
});
|
||||
await this.updateActivitiesCommand.run(_passedParam, options);
|
||||
await this.backfillNewOnboardingUserVarsCommand.run(_passedParam, options);
|
||||
await this.setUserVarsAccountsToReconnectCommand.run(_passedParam, options);
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillNewOnboardingUserVarsCommand } from 'src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars';
|
||||
import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command';
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetUserVarsAccountsToReconnectCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-user-vars-accounts-to-reconnect.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
import { UpdateFileFolderStructureCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command';
|
||||
import { UpgradeTo0_23Command } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FileStorageModule } from 'src/engine/integrations/file-storage/file-storage.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace, KeyValuePair], 'core'),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
FileStorageModule,
|
||||
OnboardingModule,
|
||||
TypeORMModule,
|
||||
DataSourceModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
FieldMetadataModule,
|
||||
DataSourceModule,
|
||||
WorkspaceStatusModule,
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
TypeORMModule,
|
||||
ViewModule,
|
||||
BillingModule,
|
||||
ObjectMetadataModule,
|
||||
ConnectedAccountModule,
|
||||
],
|
||||
providers: [
|
||||
UpdateFileFolderStructureCommand,
|
||||
MigrateLinkFieldsToLinksCommand,
|
||||
MigrateDomainNameFromTextToLinksCommand,
|
||||
MigrateMessageChannelSyncStatusEnumCommand,
|
||||
SetWorkspaceActivationStatusCommand,
|
||||
UpdateActivitiesCommand,
|
||||
BackfillNewOnboardingUserVarsCommand,
|
||||
SetUserVarsAccountsToReconnectCommand,
|
||||
UpgradeTo0_23Command,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_23CommandModule {}
|
||||
@ -0,0 +1,222 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { Any, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
|
||||
interface SetMessageDirectionCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE = 10;
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.24:set-message-direction',
|
||||
description: 'Set message direction',
|
||||
})
|
||||
export class SetMessageDirectionCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(SetMessageDirectionCommand.name);
|
||||
constructor(
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: SetMessageDirectionCommandOptions,
|
||||
): Promise<void> {
|
||||
let activeWorkspaceIds: string[] = [];
|
||||
|
||||
if (options.workspaceId) {
|
||||
activeWorkspaceIds = [options.workspaceId];
|
||||
} else {
|
||||
const activeWorkspaces = await this.workspaceRepository.find({
|
||||
where: {
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
...(options.workspaceId && { id: options.workspaceId }),
|
||||
},
|
||||
});
|
||||
|
||||
activeWorkspaceIds = activeWorkspaces.map((workspace) => workspace.id);
|
||||
}
|
||||
|
||||
if (!activeWorkspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Running command on ${activeWorkspaceIds.length} workspaces`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceId of activeWorkspaceIds) {
|
||||
try {
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelMessageAssociationWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await workspaceDataSource.transaction(async (transactionManager) => {
|
||||
try {
|
||||
const messageChannelMessageAssociationCount =
|
||||
await messageChannelMessageAssociationRepository.count(
|
||||
{},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < messageChannelMessageAssociationCount;
|
||||
i += MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE
|
||||
) {
|
||||
const messageChannelMessageAssociationsPage =
|
||||
await messageChannelMessageAssociationRepository.find(
|
||||
{
|
||||
where: {
|
||||
message: {
|
||||
messageParticipants: {
|
||||
role: 'from',
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
message: {
|
||||
messageParticipants: true,
|
||||
},
|
||||
messageChannel: {
|
||||
connectedAccount: true,
|
||||
},
|
||||
},
|
||||
take: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE,
|
||||
skip: i,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const { incoming, outgoing } =
|
||||
messageChannelMessageAssociationsPage.reduce(
|
||||
(
|
||||
acc: {
|
||||
incoming: string[];
|
||||
outgoing: string[];
|
||||
},
|
||||
messageChannelMessageAssociation,
|
||||
) => {
|
||||
const connectedAccountHandle =
|
||||
messageChannelMessageAssociation?.messageChannel
|
||||
?.connectedAccount?.handle;
|
||||
const connectedAccountHandleAliases =
|
||||
messageChannelMessageAssociation?.messageChannel
|
||||
?.connectedAccount?.handleAliases;
|
||||
const fromHandle =
|
||||
messageChannelMessageAssociation?.message
|
||||
?.messageParticipants?.[0]?.handle ?? '';
|
||||
|
||||
if (
|
||||
connectedAccountHandle === fromHandle ||
|
||||
connectedAccountHandleAliases?.includes(fromHandle)
|
||||
) {
|
||||
acc.outgoing.push(messageChannelMessageAssociation.id);
|
||||
} else {
|
||||
acc.incoming.push(messageChannelMessageAssociation.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ incoming: [], outgoing: [] },
|
||||
);
|
||||
|
||||
await messageChannelMessageAssociationRepository.update(
|
||||
{
|
||||
id: Any(incoming),
|
||||
},
|
||||
{
|
||||
direction: MessageDirection.INCOMING,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
await messageChannelMessageAssociationRepository.update(
|
||||
{
|
||||
id: Any(outgoing),
|
||||
},
|
||||
{
|
||||
direction: MessageDirection.OUTGOING,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const numberOfProcessedAssociations =
|
||||
i + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE;
|
||||
|
||||
if (
|
||||
numberOfProcessedAssociations %
|
||||
(MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE * 10) ===
|
||||
0 ||
|
||||
numberOfProcessedAssociations >=
|
||||
messageChannelMessageAssociationCount
|
||||
) {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Processed ${Math.min(numberOfProcessedAssociations, messageChannelMessageAssociationCount)} of ${messageChannelMessageAssociationCount} message channel message associations`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(`Running command on workspace ${workspaceId} failed`),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Migration failed for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { SetMessageDirectionCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
interface UpdateTo0_24CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.24',
|
||||
description: 'Upgrade to 0.24',
|
||||
})
|
||||
export class UpgradeTo0_24Command extends CommandRunner {
|
||||
constructor(
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly setMessagesDirectionCommand: SetMessageDirectionCommand,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description:
|
||||
'workspace id. Command runs on all active workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: UpdateTo0_24CommandOptions,
|
||||
): Promise<void> {
|
||||
await this.syncWorkspaceMetadataCommand.run(_passedParam, {
|
||||
...options,
|
||||
force: true,
|
||||
});
|
||||
await this.setMessagesDirectionCommand.run(_passedParam, options);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { SetMessageDirectionCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command';
|
||||
import { UpgradeTo0_24Command } from 'src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FileStorageModule } from 'src/engine/integrations/file-storage/file-storage.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
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 { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace, KeyValuePair], 'core'),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
FileStorageModule,
|
||||
OnboardingModule,
|
||||
TypeORMModule,
|
||||
DataSourceModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
FieldMetadataModule,
|
||||
WorkspaceStatusModule,
|
||||
TypeOrmModule.forFeature(
|
||||
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
TypeORMModule,
|
||||
],
|
||||
providers: [UpgradeTo0_24Command, SetMessageDirectionCommand],
|
||||
})
|
||||
export class UpgradeTo0_24CommandModule {}
|
||||
@ -1,128 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command';
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
|
||||
interface UpgradeCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
type VersionUpgradeMap = {
|
||||
[version: string]: CommandRunner[];
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-version',
|
||||
description: 'Upgrade to a specific version',
|
||||
})
|
||||
export class UpgradeVersionCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(UpgradeVersionCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly migrateLinkFieldsToLinksCommand: MigrateLinkFieldsToLinksCommand,
|
||||
private readonly migrateDomainNameFromTextToLinksCommand: MigrateDomainNameFromTextToLinksCommand,
|
||||
private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand,
|
||||
private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand,
|
||||
private readonly updateActivitiesCommand: UpdateActivitiesCommand,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-v, --version <version>',
|
||||
description: 'Version to upgrade to',
|
||||
required: true,
|
||||
})
|
||||
parseVersion(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
passedParams: string[],
|
||||
options: UpgradeCommandOptions & { version: string },
|
||||
): Promise<void> {
|
||||
const { version, ...upgradeOptions } = options;
|
||||
|
||||
const versionUpgradeMap = {
|
||||
'0.23': [
|
||||
this.migrateLinkFieldsToLinksCommand,
|
||||
this.migrateDomainNameFromTextToLinksCommand,
|
||||
this.migrateMessageChannelSyncStatusEnumCommand,
|
||||
this.setWorkspaceActivationStatusCommand,
|
||||
this.updateActivitiesCommand,
|
||||
],
|
||||
};
|
||||
|
||||
await this.validateVersions(version, versionUpgradeMap);
|
||||
|
||||
if (!versionUpgradeMap[version]) {
|
||||
throw new Error(
|
||||
`No migration commands found for version ${version}. This could mean there were no database changes required for this version.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const command of versionUpgradeMap[version]) {
|
||||
await command.run(passedParams, upgradeOptions);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Successfully upgraded to version ${version}`));
|
||||
}
|
||||
|
||||
private async getCurrentCodeVersion(): Promise<string> {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
private async validateVersions(
|
||||
targetVersion: string,
|
||||
versionUpgradeMap: VersionUpgradeMap,
|
||||
): Promise<void> {
|
||||
const currentVersion = await this.getCurrentCodeVersion();
|
||||
|
||||
const cleanCurrentVersion = semver.coerce(currentVersion);
|
||||
const cleanTargetVersion = semver.coerce(targetVersion);
|
||||
|
||||
if (!cleanCurrentVersion || !cleanTargetVersion) {
|
||||
throw new Error(
|
||||
`Invalid version format. Current Code: ${currentVersion}, Target: ${targetVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetMajorMinor = `${cleanTargetVersion.major}.${cleanTargetVersion.minor}`;
|
||||
|
||||
if (
|
||||
semver.gt(cleanTargetVersion, cleanCurrentVersion) &&
|
||||
isUndefined(versionUpgradeMap[targetMajorMinor])
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot upgrade to ${cleanTargetVersion}. Your current code version is ${cleanCurrentVersion}. Please update your codebase or upgrade your Docker image first.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Current Code Version: ${currentVersion}, Target: ${targetVersion}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||
import { BackfillNewOnboardingUserVarsCommand } from 'src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars';
|
||||
import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command';
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
import { UpgradeVersionCommand } from 'src/database/commands/upgrade-version/upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.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 { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceManagerModule,
|
||||
DataSourceModule,
|
||||
OnboardingModule,
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature(
|
||||
[Workspace, BillingSubscription, FeatureFlagEntity],
|
||||
'core',
|
||||
),
|
||||
TypeOrmModule.forFeature(
|
||||
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
WorkspaceStatusModule,
|
||||
ObjectMetadataModule,
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
FieldMetadataModule,
|
||||
ViewModule,
|
||||
BillingModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeVersionCommand,
|
||||
MigrateLinkFieldsToLinksCommand,
|
||||
MigrateDomainNameFromTextToLinksCommand,
|
||||
MigrateMessageChannelSyncStatusEnumCommand,
|
||||
SetWorkspaceActivationStatusCommand,
|
||||
UpdateActivitiesCommand,
|
||||
BackfillNewOnboardingUserVarsCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeVersionModule {}
|
||||
Reference in New Issue
Block a user