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:
Raphaël Bosi
2024-08-27 19:11:04 +02:00
committed by GitHub
parent 5ce1e6b07d
commit e771793626
26 changed files with 358 additions and 2346 deletions

View File

@ -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,

View File

@ -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!`));
}
}

View File

@ -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;`,
);
}
}

View File

@ -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}"`,
);
}
}

View File

@ -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!`));
}
}

View File

@ -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!`));
}
}

View File

@ -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!`));
}
}

View File

@ -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();
}
}
}
}
}

View File

@ -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),
);
}
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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!`));
}
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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}`,
);
}
}

View File

@ -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 {}