mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 21:50:43 +03:00
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:
parent
5ce1e6b07d
commit
e771793626
@ -1711,20 +1711,20 @@ export const getObjectMetadataItemsMock = () => {
|
||||
"updatedAt": "2024-08-05T16:38:57.285Z",
|
||||
"fromRelationMetadata": null,
|
||||
"toRelationMetadata": null,
|
||||
"defaultValue": "'incoming'",
|
||||
"defaultValue": "'INCOMING'",
|
||||
"options": [
|
||||
{
|
||||
"id": "14216544-33d1-47d0-99a9-717763d49c0f",
|
||||
"color": "green",
|
||||
"label": "Incoming",
|
||||
"value": "incoming",
|
||||
"value": "INCOMING",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": "f1b89e48-1107-45a2-b54a-31a75e76b9b2",
|
||||
"color": "blue",
|
||||
"label": "Outgoing",
|
||||
"value": "outgoing",
|
||||
"value": "OUTGOING",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
|
@ -866,20 +866,20 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
||||
"isNullable": false,
|
||||
"createdAt": "2024-08-02T16:00:05.938Z",
|
||||
"updatedAt": "2024-08-02T16:00:05.938Z",
|
||||
"defaultValue": "'incoming'",
|
||||
"defaultValue": "'INCOMING'",
|
||||
"options": [
|
||||
{
|
||||
"id": "09fd3f5f-5903-4a3a-8f8b-335825349389",
|
||||
"color": "green",
|
||||
"label": "Incoming",
|
||||
"value": "incoming",
|
||||
"value": "INCOMING",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": "0df4272e-dfef-450e-84b7-d1477e66ee7f",
|
||||
"color": "blue",
|
||||
"label": "Outgoing",
|
||||
"value": "outgoing",
|
||||
"value": "OUTGOING",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
|
@ -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 {}
|
@ -1,6 +1,7 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { DEV_SEED_MESSAGE_THREAD_IDS } from 'src/database/typeorm-seeds/workspace/message-threads';
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
|
||||
const tableName = 'message';
|
||||
|
||||
@ -39,7 +40,7 @@ export const seedMessage = async (
|
||||
receivedAt: new Date(),
|
||||
text: 'Hello, \n I hope this email finds you well. I am writing to request a meeting. I believe it would be beneficial for both parties to collaborate and explore potential opportunities. Would you be available for a meeting sometime next week? Please let me know your availability, and I will arrange a suitable time. \n Looking forward to your response.\n Best regards',
|
||||
subject: 'Meeting Request',
|
||||
direction: 'outgoing',
|
||||
direction: MessageDirection.OUTGOING,
|
||||
messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_1,
|
||||
headerMessageId: '99ef24a8-2b8a-405d-8f42-e820ca921421',
|
||||
},
|
||||
@ -51,7 +52,7 @@ export const seedMessage = async (
|
||||
receivedAt: new Date(),
|
||||
text: 'Good Morning,\n I am writing to inquire about information. Could you please provide me with details regarding this topic? \n Your assistance in this matter would be greatly appreciated. Thank you in advance for your prompt response. \n Best regards,Tim',
|
||||
subject: 'Inquiry Regarding Topic',
|
||||
direction: 'outgoing',
|
||||
direction: MessageDirection.OUTGOING,
|
||||
messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_2,
|
||||
headerMessageId: '8f804a9a-04c8-4f24-93f2-764948e95014',
|
||||
},
|
||||
@ -63,7 +64,7 @@ export const seedMessage = async (
|
||||
receivedAt: new Date(),
|
||||
text: 'Good Evening,\nI wanted to extend my sincere gratitude for taking the time to meet with me earlier today. It was a pleasure discussing with you, and I am excited about the potential opportunities for collaboration. \n Please feel free to reach out if you have any further questions or require additional information. I look forward to our continued communication. Best regards.',
|
||||
subject: 'Thank You for the Meeting',
|
||||
direction: 'incoming',
|
||||
direction: MessageDirection.INCOMING,
|
||||
messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_1,
|
||||
headerMessageId: '3939d68a-ac6b-4f86-87a2-5f5f9d1b6481',
|
||||
},
|
||||
|
@ -0,0 +1,4 @@
|
||||
export enum MessageDirection {
|
||||
INCOMING = 'INCOMING',
|
||||
OUTGOING = 'OUTGOING',
|
||||
}
|
@ -10,8 +10,12 @@ import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import {
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS,
|
||||
MESSAGE_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@ -48,6 +52,30 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
|
||||
@WorkspaceIsNullable()
|
||||
messageThreadExternalId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.direction,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Direction',
|
||||
description: 'Message Direction',
|
||||
icon: 'IconDirection',
|
||||
options: [
|
||||
{
|
||||
value: MessageDirection.INCOMING,
|
||||
label: 'Incoming',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: MessageDirection.OUTGOING,
|
||||
label: 'Outgoing',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
defaultValue: MessageDirection.INCOMING,
|
||||
})
|
||||
direction: MessageDirection;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId:
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.messageChannel,
|
||||
|
@ -40,20 +40,6 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
headerMessageId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.direction,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Direction',
|
||||
description: 'Message Direction',
|
||||
icon: 'IconDirection',
|
||||
options: [
|
||||
{ value: 'incoming', label: 'Incoming', position: 0, color: 'green' },
|
||||
{ value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' },
|
||||
],
|
||||
defaultValue: "'incoming'",
|
||||
})
|
||||
direction: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: MESSAGE_STANDARD_FIELD_IDS.subject,
|
||||
type: FieldMetadataType.TEXT,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
|
||||
export const computeMessageDirection = (
|
||||
fromHandle: string,
|
||||
@ -6,8 +7,8 @@ export const computeMessageDirection = (
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'handle' | 'handleAliases'
|
||||
>,
|
||||
): 'outgoing' | 'incoming' =>
|
||||
): MessageDirection =>
|
||||
connectedAccount.handle === fromHandle ||
|
||||
connectedAccount.handleAliases?.includes(fromHandle)
|
||||
? 'outgoing'
|
||||
: 'incoming';
|
||||
? MessageDirection.OUTGOING
|
||||
: MessageDirection.INCOMING;
|
||||
|
@ -104,7 +104,6 @@ export class MessagingMessageService {
|
||||
headerMessageId: message.headerMessageId,
|
||||
subject: message.subject,
|
||||
receivedAt: message.receivedAt,
|
||||
direction: message.direction,
|
||||
text: message.text,
|
||||
messageThreadId: newOrExistingMessageThreadId,
|
||||
},
|
||||
@ -119,6 +118,7 @@ export class MessagingMessageService {
|
||||
messageId: newMessageId,
|
||||
messageExternalId: message.externalId,
|
||||
messageThreadExternalId: message.messageThreadExternalId,
|
||||
direction: message.direction,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@ -16,6 +17,7 @@ export type Message = Omit<
|
||||
}[];
|
||||
externalId: string;
|
||||
messageThreadExternalId: string;
|
||||
direction: MessageDirection;
|
||||
};
|
||||
|
||||
export type MessageParticipant = Omit<
|
||||
|
@ -11,6 +11,7 @@ import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { CreateCompanyAndContactService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
import {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelWorkspaceEntity,
|
||||
@ -81,16 +82,16 @@ export class MessagingCreateCompanyAndContactAfterSyncJob {
|
||||
const directionFilter =
|
||||
contactAutoCreationPolicy ===
|
||||
MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED
|
||||
? Any(['incoming', 'outgoing'])
|
||||
: 'outgoing';
|
||||
? Any([MessageDirection.INCOMING, MessageDirection.OUTGOING])
|
||||
: MessageDirection.OUTGOING;
|
||||
|
||||
const contactsToCreate = await messageParticipantRepository.find({
|
||||
where: {
|
||||
message: {
|
||||
messageChannelMessageAssociations: {
|
||||
messageChannelId,
|
||||
direction: directionFilter,
|
||||
},
|
||||
direction: directionFilter,
|
||||
},
|
||||
personId: IsNull(),
|
||||
workspaceMemberId: IsNull(),
|
||||
|
Loading…
Reference in New Issue
Block a user