Remove deprecated view and related workspace entities (#15393)
# Introduction A while ago we migrated view from workspace to metadata Their standard objects workspace entities declaration remained we can now remove them ## Deprecating commands before 1.5 The view migration command from workspace to metadata was introduced in `1.5.0`. Removing the `baseWorkspaceEntity` make this command obsolete. If tomorrow twenty handles auto upgrade in latest and a user having an instance in `1.3.0` starts auto-upgrading he won't be able to migrate his views ( that's why we should not support upgrade before 1.5 anymore here ) We will have the same use case with FavoritesFolders
This commit is contained in:
-225
@@ -1,225 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Equal, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:0-54:clean-not-found-files',
|
||||
description: 'Clean not found files',
|
||||
})
|
||||
export class CleanNotFoundFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
await this.cleanNotFoundFiles(workspaceId, !!options.dryRun);
|
||||
}
|
||||
|
||||
private async cleanNotFoundFiles(workspaceId: string, dryRun: boolean) {
|
||||
await this.cleanWorkspaceLogo(workspaceId, dryRun);
|
||||
await this.softDeleteAttachments(workspaceId, dryRun);
|
||||
await this.cleanWorkspaceMembersAvatarUrl(workspaceId, dryRun);
|
||||
await this.cleanPeopleAvatarUrl(workspaceId, dryRun);
|
||||
}
|
||||
|
||||
private async checkIfFileIsFound(path: string, workspaceId: string) {
|
||||
this.logger.log(`Checking if file is found ${path}`);
|
||||
if (path.startsWith('https://')) return true; // seed data
|
||||
|
||||
const isFileFound = await this.fileStorageService.checkFileExists({
|
||||
folderPath: `workspace-${workspaceId}`,
|
||||
filename: path,
|
||||
});
|
||||
|
||||
this.logger.log(`File found: ${isFileFound}`);
|
||||
|
||||
return isFileFound;
|
||||
}
|
||||
|
||||
private async cleanWorkspaceLogo(workspaceId: string, dryRun: boolean) {
|
||||
this.logger.log(`Cleaning workspace logo for workspace ${workspaceId}`);
|
||||
const workspace = await this.workspaceRepository.findOneOrFail({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isNonEmptyString(workspace.logo)) return;
|
||||
|
||||
this.logger.log(`Processing workspace logo for workspace ${workspace.id}`);
|
||||
|
||||
const isFileFound = await this.checkIfFileIsFound(
|
||||
workspace.logo,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (isFileFound) return;
|
||||
|
||||
if (!dryRun)
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
logo: '',
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'Dry run - ' : ''}Set logo to '' for workspace ${workspace.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async softDeleteAttachments(workspaceId: string, dryRun: boolean) {
|
||||
this.logger.log(`Cleaning attachments for workspace ${workspaceId}`);
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
const attachmentsCount = await attachmentRepository.count();
|
||||
const chunkSize = 10;
|
||||
|
||||
const attachmentIdsToSoftDelete: string[] = [];
|
||||
|
||||
for (let offset = 0; offset < attachmentsCount; offset += chunkSize) {
|
||||
const attachmentsChunk = await attachmentRepository.find({
|
||||
skip: offset,
|
||||
take: chunkSize,
|
||||
});
|
||||
|
||||
const attachmentIdsToSoftDeleteChunk = await Promise.all(
|
||||
attachmentsChunk.map(async (attachment) => {
|
||||
this.logger.log(`Processing attachment ${attachment.id}`);
|
||||
const isFileFound = await this.checkIfFileIsFound(
|
||||
attachment.fullPath,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return isFileFound ? '' : attachment.id;
|
||||
}),
|
||||
);
|
||||
|
||||
attachmentIdsToSoftDelete.push(
|
||||
...attachmentIdsToSoftDeleteChunk.filter(isNonEmptyString),
|
||||
);
|
||||
}
|
||||
|
||||
if (attachmentIdsToSoftDelete.length === 0) return;
|
||||
|
||||
if (!dryRun)
|
||||
await attachmentRepository.softDelete(attachmentIdsToSoftDelete);
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'Dry run - ' : ''}Deleted attachments ${attachmentIdsToSoftDelete.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async cleanWorkspaceMembersAvatarUrl(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
) {
|
||||
this.logger.log(
|
||||
`Cleaning workspace members avatarUrl for workspace ${workspaceId}`,
|
||||
);
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
const workspaceMembers = await workspaceMemberRepository.find({
|
||||
where: {
|
||||
avatarUrl: Not(Equal('')),
|
||||
},
|
||||
});
|
||||
|
||||
const workspaceMemberIdsToUpdate: string[] = [];
|
||||
|
||||
for (const workspaceMember of workspaceMembers) {
|
||||
this.logger.log(`Processing workspaceMember ${workspaceMember.id}`);
|
||||
|
||||
const isFileFound = await this.checkIfFileIsFound(
|
||||
workspaceMember.avatarUrl,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isFileFound) continue;
|
||||
|
||||
workspaceMemberIdsToUpdate.push(workspaceMember.id);
|
||||
}
|
||||
|
||||
if (workspaceMemberIdsToUpdate.length === 0) return;
|
||||
|
||||
if (!dryRun)
|
||||
await workspaceMemberRepository.update(workspaceMemberIdsToUpdate, {
|
||||
avatarUrl: '',
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'Dry run - ' : ''}Set avatarUrl to '' for workspaceMembers ${workspaceMemberIdsToUpdate.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async cleanPeopleAvatarUrl(workspaceId: string, dryRun: boolean) {
|
||||
this.logger.log(`Cleaning people avatarUrl for workspace ${workspaceId}`);
|
||||
const personRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'person',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
const people = await personRepository.find({
|
||||
where: {
|
||||
avatarUrl: Not(Equal('')),
|
||||
},
|
||||
});
|
||||
|
||||
const personIdsToUpdate: string[] = [];
|
||||
|
||||
for (const person of people) {
|
||||
this.logger.log('Processing person', person.id);
|
||||
|
||||
const isFileFound = await this.checkIfFileIsFound(
|
||||
person.avatarUrl,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isFileFound) {
|
||||
personIdsToUpdate.push(person.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (personIdsToUpdate.length === 0) return;
|
||||
|
||||
if (!dryRun)
|
||||
await personRepository.update(personIdsToUpdate, {
|
||||
avatarUrl: '',
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'Dry run - ' : ''}Set avatarUrl to '' for people ${personIdsToUpdate.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
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 { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:0-54:0-54-created-by-default-value',
|
||||
description: 'Fix createdBy default value',
|
||||
})
|
||||
export class FixCreatedByDefaultValueCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
dataSource,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const objectsMetadataItems = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
for (const objectMetadataItem of objectsMetadataItems) {
|
||||
const createdByFieldExists = objectMetadataItem.fields.some(
|
||||
(field) => field.type === FieldMetadataType.ACTOR,
|
||||
);
|
||||
|
||||
if (!createdByFieldExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const tableName = computeTableName(
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataItem.isCustom,
|
||||
);
|
||||
|
||||
const actualDefaultValue = (
|
||||
await dataSource.query(
|
||||
`
|
||||
SELECT column_default FROM information_schema.columns
|
||||
WHERE table_schema = '${schemaName}'
|
||||
AND table_name = '${tableName}'
|
||||
AND column_name = 'createdBySource';
|
||||
`,
|
||||
undefined, // parameters
|
||||
undefined, // queryRunner
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
)
|
||||
)?.[0]?.column_default;
|
||||
|
||||
if (actualDefaultValue !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdByDefaultValues = generateDefaultValue(
|
||||
FieldMetadataType.ACTOR,
|
||||
) as ActorMetadata;
|
||||
|
||||
await dataSource.query(
|
||||
`
|
||||
ALTER TABLE "${schemaName}"."${tableName}"
|
||||
ALTER COLUMN "createdBySource" SET DEFAULT ${createdByDefaultValues.source},
|
||||
ALTER COLUMN "createdByName" SET DEFAULT ${createdByDefaultValues.name},
|
||||
ALTER COLUMN "createdByContext" SET DEFAULT '${JSON.stringify(createdByDefaultValues.context)}';
|
||||
`,
|
||||
undefined, // parameters
|
||||
undefined, // queryRunner
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:0-54:fix-standard-select-fields-position',
|
||||
description: 'Fix standard select fields position',
|
||||
})
|
||||
export class FixStandardSelectFieldsPositionCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
await this.overrideTaskStatusFieldMetadataPosition({
|
||||
workspaceId,
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
private async overrideTaskStatusFieldMetadataPosition({
|
||||
workspaceId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
dryRun: boolean | undefined;
|
||||
}) {
|
||||
const taskStatusFieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
standardId: TASK_STANDARD_FIELD_IDS.status,
|
||||
},
|
||||
});
|
||||
|
||||
if (!taskStatusFieldMetadata) {
|
||||
this.logger.warn(
|
||||
`Task status field metadata not found for workspace ${workspaceId}. Exiting.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const scannedPositions = new Set<number>();
|
||||
let biggestPosition = -1;
|
||||
|
||||
// Sort options by position for consistent processing
|
||||
const sortedOptions = (taskStatusFieldMetadata.options ?? []).sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
for (const option of sortedOptions) {
|
||||
if (scannedPositions.has(option.position)) {
|
||||
this.logger.warn(
|
||||
`Found duplicate position ${option.position} for option ${option.value} in task status field metadata for workspace ${workspaceId}.`,
|
||||
);
|
||||
|
||||
option.position = biggestPosition + 1;
|
||||
}
|
||||
|
||||
biggestPosition = Math.max(biggestPosition, option.position);
|
||||
scannedPositions.add(option.position);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.fieldMetadataRepository.update(
|
||||
{
|
||||
workspaceId,
|
||||
standardId: TASK_STANDARD_FIELD_IDS.status,
|
||||
},
|
||||
{
|
||||
options: sortedOptions,
|
||||
},
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Raw, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import {
|
||||
AppTokenEntity,
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:0-54:lowercase-user-and-invitation-emails',
|
||||
description: 'Lowercase user and invitation emails',
|
||||
})
|
||||
export class LowercaseUserAndInvitationEmailsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(UserEntity)
|
||||
protected readonly userRepository: Repository<UserEntity>,
|
||||
@InjectRepository(AppTokenEntity)
|
||||
protected readonly appTokenRepository: Repository<AppTokenEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
await this.lowercaseUserEmails(workspaceId, !!options.dryRun);
|
||||
await this.lowercaseInvitationEmails(workspaceId, !!options.dryRun);
|
||||
}
|
||||
|
||||
private async lowercaseUserEmails(workspaceId: string, dryRun: boolean) {
|
||||
const users = await this.userRepository.find({
|
||||
where: {
|
||||
userWorkspaces: {
|
||||
workspaceId,
|
||||
},
|
||||
email: Raw((alias) => `LOWER(${alias}) != ${alias}`),
|
||||
},
|
||||
});
|
||||
|
||||
if (users.length === 0) return;
|
||||
|
||||
for (const user of users) {
|
||||
if (!dryRun) {
|
||||
await this.userRepository.update(
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
{
|
||||
email: user.email.toLowerCase(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Lowercased user email ${user.email} for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async lowercaseInvitationEmails(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
) {
|
||||
const appTokens = await this.appTokenRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: AppTokenType.InvitationToken,
|
||||
context: Raw((_) => `LOWER(context->>'email') != context->>'email'`),
|
||||
},
|
||||
});
|
||||
|
||||
if (appTokens.length === 0) return;
|
||||
|
||||
for (const appToken of appTokens) {
|
||||
if (!dryRun) {
|
||||
await this.appTokenRepository.update(
|
||||
{
|
||||
id: appToken.id,
|
||||
},
|
||||
{
|
||||
context: {
|
||||
...appToken.context,
|
||||
email: appToken.context?.email.toLowerCase(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Lowercased invitation email ${appToken.context?.email} for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:0-54:migrate-default-avatar-url-to-user-workspace',
|
||||
description: 'Migrate default avatar url to user workspace',
|
||||
})
|
||||
export class MigrateDefaultAvatarUrlToUserWorkspaceCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
protected readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
await this.migrateDefaultAvatarUrlToUserWorkspace({
|
||||
workspaceId,
|
||||
dryRun: !!options.dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
private async migrateDefaultAvatarUrlToUserWorkspace({
|
||||
workspaceId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
dryRun: boolean;
|
||||
}) {
|
||||
const workspace = await this.workspaceRepository.findOneOrFail({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
||||
});
|
||||
|
||||
for (const workspaceUser of workspace.workspaceUsers) {
|
||||
if (isNonEmptyString(workspaceUser.user.defaultAvatarUrl)) {
|
||||
const userWorkspacesCount = await this.userWorkspaceRepository.count({
|
||||
where: {
|
||||
userId: workspaceUser.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (userWorkspacesCount === 1) {
|
||||
if (!dryRun)
|
||||
await this.userWorkspaceRepository.update(
|
||||
{
|
||||
userId: workspaceUser.user.id,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
{
|
||||
defaultAvatarUrl: workspaceUser.user.defaultAvatarUrl,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Updated default avatar url for user ${workspaceUser.user.id} on user workspace ${workspaceUser.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CleanNotFoundFilesCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-clean-not-found-files.command';
|
||||
import { FixCreatedByDefaultValueCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-created-by-default-value.command';
|
||||
import { FixStandardSelectFieldsPositionCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command';
|
||||
import { LowercaseUserAndInvitationEmailsCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command';
|
||||
import { MigrateDefaultAvatarUrlToUserWorkspaceCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-migrate-default-avatar-url-to-user-workspace.command';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
AppTokenEntity,
|
||||
UserEntity,
|
||||
UserWorkspaceEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity, ObjectMetadataEntity]),
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
FileModule,
|
||||
FileStorageModule,
|
||||
],
|
||||
providers: [
|
||||
FixStandardSelectFieldsPositionCommand,
|
||||
FixCreatedByDefaultValueCommand,
|
||||
CleanNotFoundFilesCommand,
|
||||
LowercaseUserAndInvitationEmailsCommand,
|
||||
MigrateDefaultAvatarUrlToUserWorkspaceCommand,
|
||||
],
|
||||
exports: [
|
||||
FixStandardSelectFieldsPositionCommand,
|
||||
FixCreatedByDefaultValueCommand,
|
||||
CleanNotFoundFilesCommand,
|
||||
LowercaseUserAndInvitationEmailsCommand,
|
||||
MigrateDefaultAvatarUrlToUserWorkspaceCommand,
|
||||
],
|
||||
})
|
||||
export class V0_54_UpgradeVersionCommandModule {}
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:0-55:deduplicate-indexed-fields',
|
||||
description: 'Deduplicate fields where we want to setup the index back on',
|
||||
})
|
||||
export class DeduplicateIndexedFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Deduplicating indexed fields for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
// searchVector should not be problematic since you cannot duplicate a search vector from what I guess
|
||||
// in company, domainName is indexed with decorator @WorkspaceIsUnique()
|
||||
// in person, email is indexed with decorator @WorkspaceIsUnique()
|
||||
// in message-channel-message-association, the object is indexed with decorator @WorkspaceIndex(['messageChannelId', 'messageId']
|
||||
// in view-field, the object is indexed with decorator @WorkspaceIndex(['fieldMetadataId', 'viewId']
|
||||
// in view-sort, the object is indexed with decorator @WorkspaceIndex(['viewId', 'fieldMetadataId']
|
||||
|
||||
// not needed since no unique constraint on this one:
|
||||
// in oportunity, stage is indexed with decorator @WorkspaceFieldIndex()
|
||||
|
||||
await this.enforceUniqueConstraintsForWorkspace(
|
||||
workspaceId,
|
||||
options.dryRun ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
private async enforceUniqueConstraintsForWorkspace(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
await this.enforceUniqueCompanyDomainName(workspaceId, dryRun);
|
||||
|
||||
await this.enforceUniquePersonEmail(workspaceId, dryRun);
|
||||
|
||||
await this.enforceUniqueMessageChannelMessageAssociation(
|
||||
workspaceId,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
await this.enforceUniqueViewField(workspaceId, dryRun);
|
||||
|
||||
await this.enforceUniqueViewSort(workspaceId, dryRun);
|
||||
}
|
||||
|
||||
private async enforceUniqueCompanyDomainName(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const companyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'company',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const duplicates = await companyRepository
|
||||
.createQueryBuilder('company')
|
||||
.select('company.domainNamePrimaryLinkUrl')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('company.deletedAt IS NULL')
|
||||
.andWhere('company.domainNamePrimaryLinkUrl IS NOT NULL')
|
||||
.andWhere("company.domainNamePrimaryLinkUrl != ''")
|
||||
.groupBy('company.domainNamePrimaryLinkUrl')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { company_domainNamePrimaryLinkUrl } = duplicate;
|
||||
const companies = await companyRepository.find({
|
||||
where: {
|
||||
domainName: {
|
||||
primaryLinkUrl: company_domainNamePrimaryLinkUrl,
|
||||
},
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < companies.length; i++) {
|
||||
const newdomainNamePrimaryLinkUrl = `${company_domainNamePrimaryLinkUrl}${i}`;
|
||||
|
||||
if (!dryRun) {
|
||||
await companyRepository.update(companies[i].id, {
|
||||
domainNamePrimaryLinkUrl: newdomainNamePrimaryLinkUrl,
|
||||
});
|
||||
}
|
||||
this.logger.log(
|
||||
`Updated company ${companies[i].id} domainName from ${company_domainNamePrimaryLinkUrl} to ${newdomainNamePrimaryLinkUrl}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniquePersonEmail(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const personRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'person',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const duplicates = await personRepository
|
||||
.createQueryBuilder('person')
|
||||
.select('person.emailsPrimaryEmail')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('person.deletedAt IS NULL')
|
||||
.andWhere('person.emailsPrimaryEmail IS NOT NULL')
|
||||
.andWhere("person.emailsPrimaryEmail != ''")
|
||||
.groupBy('person.emailsPrimaryEmail')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { person_emailsPrimaryEmail } = duplicate;
|
||||
const persons = await personRepository.find({
|
||||
where: {
|
||||
emails: {
|
||||
primaryEmail: person_emailsPrimaryEmail,
|
||||
},
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < persons.length; i++) {
|
||||
const newEmail = person_emailsPrimaryEmail?.includes('@')
|
||||
? `${person_emailsPrimaryEmail.split('@')[0]}+${i}@${person_emailsPrimaryEmail.split('@')[1]}`
|
||||
: `${person_emailsPrimaryEmail}+${i}`;
|
||||
|
||||
if (!dryRun) {
|
||||
await personRepository.update(persons[i].id, {
|
||||
emailsPrimaryEmail: newEmail,
|
||||
});
|
||||
}
|
||||
this.logger.log(
|
||||
`Updated person ${persons[i].id} emailsPrimaryEmail from ${person_emailsPrimaryEmail} to ${newEmail}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueMessageChannelMessageAssociation(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const duplicates = await messageChannelMessageAssociationRepository
|
||||
.createQueryBuilder('messageChannelMessageAssociation')
|
||||
.select('messageChannelMessageAssociation.messageId')
|
||||
.addSelect('messageChannelMessageAssociation.messageChannelId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('messageChannelMessageAssociation.deletedAt IS NULL')
|
||||
.groupBy('messageChannelMessageAssociation.messageId')
|
||||
.addGroupBy('messageChannelMessageAssociation.messageChannelId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const {
|
||||
messageChannelMessageAssociation_messageId,
|
||||
messageChannelMessageAssociation_messageChannelId,
|
||||
} = duplicate;
|
||||
const messageChannelMessageAssociations =
|
||||
await messageChannelMessageAssociationRepository.find({
|
||||
where: {
|
||||
messageId: messageChannelMessageAssociation_messageId,
|
||||
messageChannelId: messageChannelMessageAssociation_messageChannelId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < messageChannelMessageAssociations.length; i++) {
|
||||
if (!dryRun) {
|
||||
await messageChannelMessageAssociationRepository.delete(
|
||||
messageChannelMessageAssociations[i].id,
|
||||
);
|
||||
}
|
||||
this.logger.log(
|
||||
`Deleted messageChannelMessageAssociation ${messageChannelMessageAssociations[i].id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueViewField(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
|
||||
const duplicates = await viewFieldRepository
|
||||
.createQueryBuilder('viewField')
|
||||
.select('viewField.fieldMetadataId')
|
||||
.addSelect('viewField.viewId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('viewField.deletedAt IS NULL')
|
||||
.groupBy('viewField.fieldMetadataId')
|
||||
.addGroupBy('viewField.viewId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { viewField_fieldMetadataId, viewField_viewId } = duplicate;
|
||||
const viewFields = await viewFieldRepository.find({
|
||||
where: {
|
||||
fieldMetadataId: viewField_fieldMetadataId,
|
||||
viewId: viewField_viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < viewFields.length; i++) {
|
||||
if (!dryRun) {
|
||||
await viewFieldRepository.delete(viewFields[i].id);
|
||||
}
|
||||
this.logger.log(`Deleted viewField ${viewFields[i].id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enforceUniqueViewSort(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewSortRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewSort',
|
||||
);
|
||||
|
||||
const duplicates = await viewSortRepository
|
||||
.createQueryBuilder('viewSort')
|
||||
.select('viewSort.viewId')
|
||||
.addSelect('viewSort.fieldMetadataId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('viewSort.deletedAt IS NULL')
|
||||
.groupBy('viewSort.viewId')
|
||||
.addGroupBy('viewSort.fieldMetadataId')
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const { viewSort_viewId, viewSort_fieldMetadataId } = duplicate;
|
||||
const viewSorts = await viewSortRepository.find({
|
||||
where: {
|
||||
fieldMetadataId: viewSort_fieldMetadataId,
|
||||
viewId: viewSort_viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
for (let i = 1; i < viewSorts.length; i++) {
|
||||
if (!dryRun) {
|
||||
await viewSortRepository.delete(viewSorts[i].id);
|
||||
}
|
||||
this.logger.log(`Deleted viewSort ${viewSorts[i].id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DeduplicateIndexedFieldsCommand } from 'src/database/commands/upgrade-version-command/0-55/0-55-deduplicate-indexed-fields.command';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
AppTokenEntity,
|
||||
UserEntity,
|
||||
UserWorkspaceEntity,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [DeduplicateIndexedFieldsCommand],
|
||||
exports: [DeduplicateIndexedFieldsCommand],
|
||||
})
|
||||
export class V0_55_UpgradeVersionCommandModule {}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { WORKFLOW_RUN_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-1:add-enqueued-status-to-workflow-run',
|
||||
description: 'Add enqueued status to workflow run',
|
||||
})
|
||||
export class AddEnqueuedStatusToWorkflowRunCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Adding enqueued status to workflow run for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workflowRunStatusFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.status,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflowRunStatusFieldMetadata) {
|
||||
this.logger.error(
|
||||
`Workflow run status field metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowRunStatusFieldMetadataOptions =
|
||||
workflowRunStatusFieldMetadata.options;
|
||||
|
||||
// check if enqueued status is already in the field metadata options
|
||||
if (
|
||||
workflowRunStatusFieldMetadataOptions?.some(
|
||||
(option) => option.value === WorkflowRunStatus.ENQUEUED,
|
||||
)
|
||||
) {
|
||||
this.logger.log(
|
||||
`Workflow run status field metadata options already contain enqueued status for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would add enqueued status to workflow run status field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
workflowRunStatusFieldMetadataOptions?.push({
|
||||
value: WorkflowRunStatus.ENQUEUED,
|
||||
label: 'Enqueued',
|
||||
position: 4,
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
await this.fieldMetadataRepository.save(workflowRunStatusFieldMetadata);
|
||||
|
||||
this.logger.log(
|
||||
`Enqueued status added to workflow run status field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would try to add enqueued status to workflow run status enum for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await this.coreDataSource.query(
|
||||
`ALTER TYPE ${schemaName}."workflowRun_status_enum" ADD VALUE 'ENQUEUED'`,
|
||||
);
|
||||
this.logger.log(
|
||||
`Enqueued status added to workflow run status enum for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error adding enqueued status to workflow run status enum for workspace ${workspaceId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-1:fix-schema-array-type',
|
||||
description: 'Fix columns for ARRAY fields to be text[] in the DB schema',
|
||||
})
|
||||
export class FixSchemaArrayTypeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly databaseStructureService: DatabaseStructureService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(`Fixing ARRAY field columns for workspace ${workspaceId}`);
|
||||
|
||||
const arrayFields: FieldMetadataEntity[] =
|
||||
await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.ARRAY,
|
||||
isCustom: true,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
if (arrayFields.length === 0) {
|
||||
this.logger.log('No ARRAY fields found for this workspace.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const field of arrayFields) {
|
||||
const object = field.object;
|
||||
|
||||
const tableName = computeObjectTargetTable(object);
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
const columns =
|
||||
await this.databaseStructureService.getWorkspaceTableColumns(
|
||||
schemaName,
|
||||
tableName,
|
||||
);
|
||||
const columnName = computeColumnName(field);
|
||||
const dbColumn = columns.find((col) => col.columnName === columnName);
|
||||
|
||||
if (!dbColumn) {
|
||||
this.logger.warn(
|
||||
`Column ${columnName} not found in table ${schemaName}.${tableName}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (dbColumn.dataType === 'text[]' && dbColumn.isArray) {
|
||||
continue;
|
||||
}
|
||||
this.logger.log(
|
||||
`Altering column ${schemaName}.${tableName}.${columnName} to type text[] (was ${dbColumn.dataType})`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE text[] USING "${columnName}"::text[];`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-1:fix-update-standard-fields-is-label-synced-with-name',
|
||||
description:
|
||||
'Fix isLabelSyncedWithName property for standard fields to match actual label-name synchronization state',
|
||||
})
|
||||
export class FixUpdateStandardFieldsIsLabelSyncedWithName extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(`Updating standard fields for workspace ${workspaceId}`);
|
||||
const workspaceStandardFields = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
isCustom: false,
|
||||
standardId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
let updatedFields = 0;
|
||||
|
||||
for (const field of workspaceStandardFields) {
|
||||
const isLabelSyncedWithName =
|
||||
computeMetadataNameFromLabel(field.label) === field.name;
|
||||
|
||||
if (field.isLabelSyncedWithName === isLabelSyncedWithName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.fieldMetadataRepository.update(field.id, {
|
||||
isLabelSyncedWithName,
|
||||
});
|
||||
}
|
||||
updatedFields++;
|
||||
this.logger.log(`Updated isLabelSyncedMetadata for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (!options.dryRun && updatedFields > 0) {
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
this.logger.log(
|
||||
`Updated ${updatedFields} field.s for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { MoreThan, Repository } from 'typeorm';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import {
|
||||
type WorkflowRunState,
|
||||
type WorkflowRunOutput,
|
||||
type WorkflowRunWorkspaceEntity,
|
||||
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 500;
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-1:migrate-workflow-run-state',
|
||||
description: 'Migrate state column in workflow run records',
|
||||
})
|
||||
export class MigrateWorkflowRunStatesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private afterDate: string | undefined;
|
||||
private chunkSize = DEFAULT_CHUNK_SIZE;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--after-date [after_date]',
|
||||
description: 'Only select records after this date (YYYY-MM-DD).',
|
||||
required: false,
|
||||
})
|
||||
parseAfterDate(val: string): string | undefined {
|
||||
const date = new Date(val);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error(`Invalid date format: ${val}`);
|
||||
}
|
||||
|
||||
const afterDate = date.toISOString();
|
||||
|
||||
this.afterDate = afterDate;
|
||||
|
||||
return afterDate;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--chunk-size [chunk_size]',
|
||||
description:
|
||||
'Split workflowRuns into chunks for each workspaces (default 500)',
|
||||
required: false,
|
||||
})
|
||||
parseChunkSize(val: number): number {
|
||||
if (isNaN(val) || val <= 0) {
|
||||
throw new Error(`Invalid chunk size: ${val}. Should be greater than 0`);
|
||||
}
|
||||
|
||||
this.chunkSize = val;
|
||||
|
||||
return this.chunkSize;
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunCount = await workflowRunRepository.count();
|
||||
|
||||
const chunkCount = Math.ceil(workflowRunCount / this.chunkSize);
|
||||
|
||||
this.logger.log(
|
||||
`Migrate ${workflowRunCount} workflowRun state in ${chunkCount} chunks of size ${this.chunkSize}`,
|
||||
);
|
||||
|
||||
for (let offset = 0; offset < chunkCount; offset += 1) {
|
||||
this.logger.log(`- Proceeding chunk ${offset + 1}/${chunkCount}`);
|
||||
|
||||
const findOption = isDefined(this.afterDate)
|
||||
? { where: { startedAt: MoreThan(this.afterDate) } }
|
||||
: {};
|
||||
|
||||
const workflowRuns = (await workflowRunRepository.find({
|
||||
...findOption,
|
||||
skip: offset * this.chunkSize,
|
||||
take: this.chunkSize,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
})) as any[]; // We type as any as workflowRun output has been removed since 1.1.0 release
|
||||
|
||||
for (const workflowRun of workflowRuns) {
|
||||
const output = workflowRun.output;
|
||||
|
||||
if (!isDefined(output)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const state = this.buildRunStateFromOutput(output);
|
||||
|
||||
await workflowRunRepository.update(workflowRun.id, {
|
||||
state,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildRunStateFromOutput(output: WorkflowRunOutput): WorkflowRunState {
|
||||
const stepInfos: WorkflowRunStepInfos = Object.fromEntries(
|
||||
output.flow.steps.map((step) => {
|
||||
const stepOutput = output.stepsOutput?.[step.id];
|
||||
const status = stepOutput?.pendingEvent
|
||||
? StepStatus.PENDING
|
||||
: stepOutput?.error
|
||||
? StepStatus.FAILED
|
||||
: stepOutput?.result
|
||||
? StepStatus.SUCCESS
|
||||
: StepStatus.NOT_STARTED;
|
||||
|
||||
return [
|
||||
step.id,
|
||||
{
|
||||
result: stepOutput?.result,
|
||||
error: stepOutput?.error,
|
||||
status,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
stepInfos['trigger'] = {
|
||||
result: output?.stepsOutput?.trigger?.result,
|
||||
status: StepStatus.SUCCESS,
|
||||
};
|
||||
|
||||
return {
|
||||
flow: output?.flow,
|
||||
workflowRunError: output?.error,
|
||||
stepInfos,
|
||||
};
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command';
|
||||
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
|
||||
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
|
||||
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { WebhookModule } from 'src/engine/core-modules/webhook/webhook.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
AppTokenEntity,
|
||||
UserEntity,
|
||||
UserWorkspaceEntity,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
ApiKeyEntity,
|
||||
WebhookEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
WorkspaceHealthModule,
|
||||
TypeORMModule,
|
||||
ApiKeyModule,
|
||||
WebhookModule,
|
||||
],
|
||||
providers: [
|
||||
FixUpdateStandardFieldsIsLabelSyncedWithName,
|
||||
FixSchemaArrayTypeCommand,
|
||||
MigrateWorkflowRunStatesCommand,
|
||||
AddEnqueuedStatusToWorkflowRunCommand,
|
||||
],
|
||||
exports: [
|
||||
FixUpdateStandardFieldsIsLabelSyncedWithName,
|
||||
FixSchemaArrayTypeCommand,
|
||||
MigrateWorkflowRunStatesCommand,
|
||||
AddEnqueuedStatusToWorkflowRunCommand,
|
||||
],
|
||||
})
|
||||
export class V1_1_UpgradeVersionCommandModule {}
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { WORKFLOW_RUN_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 { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-2:add-enqueued-status-to-workflow-run-v2',
|
||||
description: 'Add enqueued status to workflow run',
|
||||
})
|
||||
export class AddEnqueuedStatusToWorkflowRunV2Command extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Adding enqueued status to workflow run for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workflowRunObjectMetadata =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
standardId: STANDARD_OBJECT_IDS.workflowRun,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflowRunObjectMetadata) {
|
||||
this.logger.error(
|
||||
`Workflow run object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowRunStatusFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.status,
|
||||
objectMetadataId: workflowRunObjectMetadata.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflowRunStatusFieldMetadata) {
|
||||
this.logger.error(
|
||||
`Workflow run status field metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowRunStatusFieldMetadataOptions =
|
||||
workflowRunStatusFieldMetadata.options;
|
||||
|
||||
// check if enqueued status is already in the field metadata options
|
||||
if (
|
||||
workflowRunStatusFieldMetadataOptions?.some(
|
||||
(option) => option.value === WorkflowRunStatus.ENQUEUED,
|
||||
)
|
||||
) {
|
||||
this.logger.log(
|
||||
`Workflow run status field metadata options already contain enqueued status for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would add enqueued status to workflow run status field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
workflowRunStatusFieldMetadataOptions?.push({
|
||||
value: WorkflowRunStatus.ENQUEUED,
|
||||
label: 'Enqueued',
|
||||
position: 4,
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
await this.fieldMetadataRepository.save(workflowRunStatusFieldMetadata);
|
||||
|
||||
this.logger.log(
|
||||
`Enqueued status added to workflow run status field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would try to add enqueued status to workflow run status enum for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await this.coreDataSource.query(
|
||||
`ALTER TYPE ${schemaName}."workflowRun_status_enum" ADD VALUE 'ENQUEUED'`,
|
||||
);
|
||||
this.logger.log(
|
||||
`Enqueued status added to workflow run status enum for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error adding enqueued status to workflow run status enum for workspace ${workspaceId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-2:add-next-step-ids-to-workflow-version-triggers',
|
||||
description: 'Add next step ids to workflow version triggers',
|
||||
})
|
||||
export class AddNextStepIdsToWorkflowVersionTriggers extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersions = await workflowVersionRepository.find();
|
||||
|
||||
for (const workflowVersion of workflowVersions) {
|
||||
try {
|
||||
const { trigger, steps } = workflowVersion;
|
||||
|
||||
if (!isDefined(trigger)) {
|
||||
this.logger.warn(
|
||||
`Undefined trigger for workflowVersion ${workflowVersion.id}`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(steps)) {
|
||||
this.logger.warn(
|
||||
`Undefined steps for workflowVersion ${workflowVersion.id}`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const rootSteps = this.getRootSteps(steps);
|
||||
|
||||
if (rootSteps.length === 0) {
|
||||
this.logger.warn(
|
||||
`No root steps found for workflowVersion ${workflowVersion.id}`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
trigger: {
|
||||
...trigger,
|
||||
nextStepIds: rootSteps.map((step) => step.id),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error while adding nextStepIds to workflowVersion ${workflowVersion.id}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`${workflowVersions.length} triggers updated`);
|
||||
}
|
||||
|
||||
private getRootSteps(steps: WorkflowAction[]): WorkflowAction[] {
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
return steps.filter((step) => !childIds.has(step.id));
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'migrate:1-2:remove-workflow-runs-without-state',
|
||||
description: 'Remove workflow runs without state.',
|
||||
})
|
||||
export class RemoveWorkflowRunsWithoutState extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await workflowRunRepository.delete({ state: IsNull() });
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
|
||||
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
|
||||
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
],
|
||||
providers: [
|
||||
RemoveWorkflowRunsWithoutState,
|
||||
AddEnqueuedStatusToWorkflowRunV2Command,
|
||||
AddNextStepIdsToWorkflowVersionTriggers,
|
||||
],
|
||||
exports: [
|
||||
RemoveWorkflowRunsWithoutState,
|
||||
AddEnqueuedStatusToWorkflowRunV2Command,
|
||||
AddNextStepIdsToWorkflowVersionTriggers,
|
||||
],
|
||||
})
|
||||
export class V1_2_UpgradeVersionCommandModule {}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-3:add-next-step-ids-to-workflow-runs-trigger',
|
||||
description: 'Add next step ids to workflow runs trigger',
|
||||
})
|
||||
export class AddNextStepIdsToWorkflowRunsTrigger extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const workflowRuns = await this.coreDataSource.query(
|
||||
`SELECT id, state FROM ${schemaName}."workflowRun"`,
|
||||
);
|
||||
|
||||
let updatedWorkflowRunCount = 0;
|
||||
|
||||
for (const workflowRun of workflowRuns) {
|
||||
try {
|
||||
if (isDefined(workflowRun.state.flow.trigger.nextStepIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rootSteps = this.getRootSteps(workflowRun.state.flow.steps || []);
|
||||
|
||||
const nextStepIds = rootSteps.map((step) => step.id);
|
||||
|
||||
const updatedState = {
|
||||
...workflowRun.state,
|
||||
flow: {
|
||||
...workflowRun.state.flow,
|
||||
trigger: {
|
||||
...workflowRun.state.flow.trigger,
|
||||
nextStepIds,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await this.coreDataSource.query(
|
||||
`UPDATE ${schemaName}."workflowRun" SET state = $1::jsonb WHERE id = $2;`,
|
||||
[updatedState, workflowRun.id],
|
||||
);
|
||||
|
||||
updatedWorkflowRunCount += 1;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error while adding nextStepIds to workflowRun state '${workflowRun.id}'`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.logger.log(
|
||||
`${updatedWorkflowRunCount}/${workflowRuns.length} triggers updated`,
|
||||
);
|
||||
}
|
||||
|
||||
private getRootSteps(steps: WorkflowAction[]): WorkflowAction[] {
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
return steps.filter((step) => !childIds.has(step.id));
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-3:update-timestamp-column-type-in-workspace-schema',
|
||||
description:
|
||||
'Update the timestamp column type in all the workspace schema tables',
|
||||
})
|
||||
export class UpdateTimestampColumnTypeInWorkspaceSchemaCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const dateTimeFieldMetadataItems = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
for (const fieldMetadataItem of dateTimeFieldMetadataItems) {
|
||||
this.logger.log(
|
||||
`Updating column type for ${fieldMetadataItem.name} in ${schemaName}."${computeObjectTargetTable(fieldMetadataItem.object)}"`,
|
||||
);
|
||||
await this.coreDataSource.query(
|
||||
`ALTER TABLE ${schemaName}."${computeObjectTargetTable(fieldMetadataItem.object)}"
|
||||
ALTER COLUMN "${fieldMetadataItem.name}" TYPE timestamptz(3);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command';
|
||||
import { UpdateTimestampColumnTypeInWorkspaceSchemaCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-update-timestamp-column-type-in-workspace-schema.command';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ApiKeyEntity,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
RoleEntity,
|
||||
RoleTargetsEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
ApiKeyModule,
|
||||
FeatureFlagModule,
|
||||
TwentyORMModule,
|
||||
RoleModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceFeatureFlagsMapCacheModule,
|
||||
],
|
||||
providers: [
|
||||
AddNextStepIdsToWorkflowRunsTrigger,
|
||||
UpdateTimestampColumnTypeInWorkspaceSchemaCommand,
|
||||
],
|
||||
exports: [
|
||||
AddNextStepIdsToWorkflowRunsTrigger,
|
||||
UpdateTimestampColumnTypeInWorkspaceSchemaCommand,
|
||||
],
|
||||
})
|
||||
export class V1_3_UpgradeVersionCommandModule {}
|
||||
-333
@@ -1,333 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import Dagre from '@dagrejs/dagre';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
type Node = {
|
||||
id: string;
|
||||
position: { x: number; y: number };
|
||||
size: number;
|
||||
measured?: { width: number; height: number };
|
||||
};
|
||||
|
||||
type Edge = { id: string; source: string; target: string };
|
||||
|
||||
type Diagram = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
export type AddPositionsToWorkflowVersionsAndWorkflowRunsOptions =
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions & {
|
||||
processWorkflowRuns?: boolean;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-5:add-positions-to-workflow-versions-and-workflow-runs',
|
||||
description: 'Add positions to workflow versions and workflow runs',
|
||||
})
|
||||
export class AddPositionsToWorkflowVersionsAndWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--process-workflow-runs [process_workflow_runs]',
|
||||
description: 'Process workflowRuns positions (default false)',
|
||||
required: false,
|
||||
})
|
||||
parseProcessWorkflowRuns(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: Omit<RunOnWorkspaceArgs, 'options'> & {
|
||||
options: AddPositionsToWorkflowVersionsAndWorkflowRunsOptions;
|
||||
}): Promise<void> {
|
||||
await this.addPositionsToWorkflowVersions({ workspaceId });
|
||||
|
||||
if (options.processWorkflowRuns) {
|
||||
await this.addPositionsToWorkflowRuns({ workspaceId });
|
||||
}
|
||||
}
|
||||
|
||||
private async addPositionsToWorkflowVersions({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersions = await workflowVersionRepository.find();
|
||||
|
||||
let count = 0;
|
||||
|
||||
for (const workflowVersion of workflowVersions) {
|
||||
try {
|
||||
// We only update one branch workflow
|
||||
if (this.isWorkflowMultiBranch(workflowVersion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { updatedTrigger, updatedSteps } =
|
||||
this.getOrganizedStepsAndTrigger({
|
||||
trigger: workflowVersion.trigger,
|
||||
steps: workflowVersion.steps,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
trigger: updatedTrigger,
|
||||
steps: updatedSteps,
|
||||
});
|
||||
count += 1;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error while adding positions to workflowVersion '${workflowVersion.id}'`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Workflow versions updated count: ${count} out of ${workflowVersions.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
private isWorkflowMultiBranch(
|
||||
workflowVersion: WorkflowVersionWorkspaceEntity,
|
||||
) {
|
||||
if ((workflowVersion.trigger?.nextStepIds ?? []).length > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const step of workflowVersion.steps || []) {
|
||||
if ((step.nextStepIds ?? []).length > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async addPositionsToWorkflowRuns({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const workflowRuns = await this.coreDataSource.query(
|
||||
`SELECT id, state FROM ${schemaName}."workflowRun"`,
|
||||
);
|
||||
|
||||
for (const workflowRun of workflowRuns) {
|
||||
try {
|
||||
const { updatedTrigger, updatedSteps } =
|
||||
this.getOrganizedStepsAndTrigger({
|
||||
trigger: workflowRun.state.flow.trigger,
|
||||
steps: workflowRun.state.flow.steps,
|
||||
});
|
||||
|
||||
const updatedState = {
|
||||
...workflowRun.state,
|
||||
flow: {
|
||||
...workflowRun.state.flow,
|
||||
trigger: updatedTrigger,
|
||||
steps: updatedSteps,
|
||||
},
|
||||
};
|
||||
|
||||
await this.coreDataSource.query(
|
||||
`UPDATE ${schemaName}."workflowRun" SET state = $1::jsonb WHERE id = $2`,
|
||||
[updatedState, workflowRun.id],
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error while adding positions to workflowRuns '${workflowRun.id}'`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getNodePositionFromDiagram({
|
||||
stepId,
|
||||
diagram,
|
||||
}: {
|
||||
stepId: string;
|
||||
diagram: Diagram;
|
||||
}) {
|
||||
return diagram.nodes.find((node) => node.id === stepId)?.position;
|
||||
}
|
||||
|
||||
private createWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
}: {
|
||||
trigger?: WorkflowTrigger | null;
|
||||
steps: WorkflowAction[] | null;
|
||||
}): Diagram {
|
||||
const nodes: Node[] = [];
|
||||
|
||||
const edges: Edge[] = [];
|
||||
|
||||
if (!isDefined(trigger)) {
|
||||
const triggerNextStepIds = isDefined(steps)
|
||||
? this.getRootSteps(steps).map((step) => step.id)
|
||||
: [];
|
||||
|
||||
triggerNextStepIds.forEach((stepId: string) => {
|
||||
edges.push({
|
||||
id: v4(),
|
||||
source: 'trigger',
|
||||
target: stepId,
|
||||
});
|
||||
});
|
||||
nodes.push({ id: 'trigger', size: 13, position: { x: 0, y: 0 } });
|
||||
} else {
|
||||
nodes.push({
|
||||
id: 'trigger',
|
||||
size: Math.min(trigger.name.length, 29),
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
for (const step of steps || []) {
|
||||
nodes.push({
|
||||
id: step.id,
|
||||
size: Math.min(step.name.length, 29),
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
step.nextStepIds?.forEach((nextStepId) => {
|
||||
edges.push({
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
target: nextStepId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const stepId of trigger?.nextStepIds || []) {
|
||||
edges.push({
|
||||
id: v4(),
|
||||
source: 'trigger',
|
||||
target: stepId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
}
|
||||
|
||||
private getOrganizedStepsAndTrigger({
|
||||
trigger,
|
||||
steps,
|
||||
}: {
|
||||
trigger?: WorkflowTrigger | null;
|
||||
steps: WorkflowAction[] | null;
|
||||
}) {
|
||||
const workflowDiagram = this.createWorkflowDiagram({ steps, trigger });
|
||||
|
||||
const organizedDiagram = this.getOrganizedDiagram(workflowDiagram);
|
||||
|
||||
const updatedTrigger: WorkflowTrigger | undefined = isDefined(trigger)
|
||||
? {
|
||||
...trigger,
|
||||
position: this.getNodePositionFromDiagram({
|
||||
stepId: 'trigger',
|
||||
diagram: organizedDiagram,
|
||||
}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const updatedSteps: WorkflowAction[] = (steps || []).map(
|
||||
(step: WorkflowAction) => ({
|
||||
...step,
|
||||
position: this.getNodePositionFromDiagram({
|
||||
stepId: step.id,
|
||||
diagram: organizedDiagram,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
updatedTrigger,
|
||||
updatedSteps,
|
||||
};
|
||||
}
|
||||
|
||||
private getRootSteps(steps: WorkflowAction[]): WorkflowAction[] {
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
return steps.filter((step) => !childIds.has(step.id));
|
||||
}
|
||||
|
||||
private getOrganizedDiagram(diagram: Diagram): Diagram {
|
||||
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
graph.setGraph({
|
||||
ranksep: 80, // Vertical distance between 2 nodes
|
||||
nodesep: 200, // Horizontal distance between 2 nodes
|
||||
rankdir: 'TB',
|
||||
});
|
||||
|
||||
diagram.edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
|
||||
diagram.nodes.forEach((node) =>
|
||||
graph.setNode(node.id, {
|
||||
width: node.size * 6,
|
||||
height: 50,
|
||||
}),
|
||||
);
|
||||
|
||||
Dagre.layout(graph);
|
||||
|
||||
return {
|
||||
nodes: diagram.nodes.map((node) => {
|
||||
const position = graph.node(node.id);
|
||||
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the React Flow node anchor point (top left).
|
||||
const x = position.x - position.width / 2;
|
||||
const y = position.y - position.height / 2;
|
||||
|
||||
return { ...node, position: { x, y } };
|
||||
}),
|
||||
edges: diagram.edges,
|
||||
};
|
||||
}
|
||||
}
|
||||
-671
@@ -1,671 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { type ViewFilterOperand as SharedViewFilterOperand } from 'twenty-shared/types';
|
||||
import { convertViewFilterOperandToCoreOperand } from 'twenty-shared/utils';
|
||||
import { DataSource, In, Repository, type QueryRunner } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator';
|
||||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { type ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
|
||||
import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in';
|
||||
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { type ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
import { convertViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/convert-view-filter-workspace-value-to-core-value';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-5:migrate-views-to-core',
|
||||
description: 'Migrate views from workspace schemas to core schema',
|
||||
})
|
||||
export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating views to core schema for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await this.migrateViews(
|
||||
workspaceId,
|
||||
options.dryRun ?? false,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would migrate views to core schema for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
`Successfully migrated views to core schema for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Transaction rolled back for workspace ${workspaceId} due to error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to migrate views to core schema for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViews(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const workspaceViewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceViews = await workspaceViewRepository.find({
|
||||
relations: [
|
||||
'viewFields',
|
||||
'viewFilters',
|
||||
'viewSorts',
|
||||
'viewGroups',
|
||||
'viewFilterGroups',
|
||||
],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const {
|
||||
corruptedViewIds,
|
||||
corruptedViewFieldIds,
|
||||
corruptedViewFilterIds,
|
||||
corruptedViewSortIds,
|
||||
corruptedViewGroupIds,
|
||||
} = await this.fetchAndDeleteCorruptedViewResources(
|
||||
workspaceId,
|
||||
workspaceViews,
|
||||
queryRunner,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
const workspaceViewWithoutOrphansResources = workspaceViews
|
||||
.filter((view) => !corruptedViewIds.includes(view.id))
|
||||
.map((view) => ({
|
||||
...view,
|
||||
viewFields: view.viewFields.filter(
|
||||
(viewField) => !corruptedViewFieldIds.includes(viewField.id),
|
||||
),
|
||||
viewFilters: view.viewFilters.filter(
|
||||
(viewFilter) => !corruptedViewFilterIds.includes(viewFilter.id),
|
||||
),
|
||||
viewSorts: view.viewSorts.filter(
|
||||
(viewSort) => !corruptedViewSortIds.includes(viewSort.id),
|
||||
),
|
||||
viewGroups: view.viewGroups.filter(
|
||||
(viewGroup) => !corruptedViewGroupIds.includes(viewGroup.id),
|
||||
),
|
||||
}));
|
||||
|
||||
if (workspaceViewWithoutOrphansResources.length === 0) {
|
||||
this.logger.log(`No views to migrate for workspace ${workspaceId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Found ${workspaceViews.length} views to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
for (const view of workspaceViewWithoutOrphansResources) {
|
||||
const deletedStatus = view.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`DRY RUN: Would migrate view ${view.id} (${view.name})${deletedStatus} with ${view.viewFields.length} fields, ${view.viewFilters.length} filters, ${view.viewSorts.length} sorts, ${view.viewGroups.length} groups, ${view.viewFilterGroups.length} filter groups`,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteExistingCoreViewObjects(workspaceId, queryRunner, dryRun);
|
||||
|
||||
for (const workspaceView of workspaceViewWithoutOrphansResources) {
|
||||
await this.migrateViewEntity(workspaceView, workspaceId, queryRunner);
|
||||
|
||||
if (workspaceView.viewFields?.length > 0) {
|
||||
await this.migrateViewFields(
|
||||
workspaceView.viewFields,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewFilterGroups?.length > 0) {
|
||||
await this.migrateViewFilterGroups(
|
||||
workspaceView.viewFilterGroups,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewFilters?.length > 0) {
|
||||
await this.migrateViewFilters(
|
||||
workspaceView.viewFilters,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewSorts?.length > 0) {
|
||||
await this.migrateViewSorts(
|
||||
workspaceView.viewSorts,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewGroups?.length > 0) {
|
||||
await this.migrateViewGroups(
|
||||
workspaceView.viewGroups,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
const deletedStatus = workspaceView.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`Migrated view ${workspaceView.id} (${workspaceView.name})${deletedStatus} to core schema`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchAndDeleteCorruptedViewResources(
|
||||
workspaceId: string,
|
||||
views: ViewWorkspaceEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
dryRun: boolean,
|
||||
): Promise<{
|
||||
corruptedViewIds: string[];
|
||||
corruptedViewFieldIds: string[];
|
||||
corruptedViewFilterIds: string[];
|
||||
corruptedViewSortIds: string[];
|
||||
corruptedViewGroupIds: string[];
|
||||
}> {
|
||||
const fieldMetadataIdsUsedInViewFields = views
|
||||
.flatMap((view) => view.viewFields)
|
||||
.map((viewField) => viewField.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIdsUsedInViewFilters = views
|
||||
.flatMap((view) => view.viewFilters)
|
||||
.map((viewFilter) => viewFilter.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIdsUsedInViewSorts = views
|
||||
.flatMap((view) => view.viewSorts)
|
||||
.map((viewSort) => viewSort.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIdsUsedInViewGroups = views
|
||||
.flatMap((view) => view.viewGroups)
|
||||
.map((viewGroup) => viewGroup.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIds = [
|
||||
...fieldMetadataIdsUsedInViewFields,
|
||||
...fieldMetadataIdsUsedInViewFilters,
|
||||
...fieldMetadataIdsUsedInViewSorts,
|
||||
...fieldMetadataIdsUsedInViewGroups,
|
||||
];
|
||||
|
||||
const fieldMetadataRepository =
|
||||
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||
const fieldMetadataItems = await fieldMetadataRepository.find({
|
||||
where: {
|
||||
id: In(fieldMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
const existingFieldMetadataIds = fieldMetadataItems.map(
|
||||
(fieldMetadata) => fieldMetadata.id,
|
||||
);
|
||||
|
||||
const corruptedViewFieldIds = views
|
||||
.flatMap((view) => view.viewFields)
|
||||
.filter(
|
||||
(viewField) =>
|
||||
!existingFieldMetadataIds.includes(viewField.fieldMetadataId) ||
|
||||
viewField.position === null,
|
||||
)
|
||||
.map((viewField) => viewField.id);
|
||||
|
||||
const corruptedViewFilterIds = views
|
||||
.flatMap((view) => view.viewFilters)
|
||||
.filter(
|
||||
(viewFilter) =>
|
||||
!existingFieldMetadataIds.includes(viewFilter.fieldMetadataId),
|
||||
)
|
||||
.map((viewFilter) => viewFilter.id);
|
||||
|
||||
const corruptedViewSortIds = views
|
||||
.flatMap((view) => view.viewSorts)
|
||||
.filter(
|
||||
(viewSort) =>
|
||||
!existingFieldMetadataIds.includes(viewSort.fieldMetadataId),
|
||||
)
|
||||
.map((viewSort) => viewSort.id);
|
||||
|
||||
const corruptedViewGroupIds = views
|
||||
.flatMap((view) => view.viewGroups)
|
||||
.filter(
|
||||
(viewGroup) =>
|
||||
!existingFieldMetadataIds.includes(viewGroup.fieldMetadataId),
|
||||
)
|
||||
.map((viewGroup) => viewGroup.id);
|
||||
|
||||
const workspaceViewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewFieldRepository.delete({
|
||||
id: In(corruptedViewFieldIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewFieldIds.length} out of ${views.flatMap((view) => view.viewFields).length} view fields that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workspaceViewFilterRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewFilter',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewFilterRepository.delete({
|
||||
id: In(corruptedViewFilterIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewFilterIds.length} out of ${views.flatMap((view) => view.viewFilters).length} view filters that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workspaceViewSortRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewSortWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewSort',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewSortRepository.delete({
|
||||
id: In(corruptedViewSortIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewSortIds.length} out of ${views.flatMap((view) => view.viewSorts).length} view sorts that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workspaceViewGroupRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewGroup',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewGroupRepository.delete({
|
||||
id: In(corruptedViewGroupIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewGroupIds.length} out of ${views.flatMap((view) => view.viewGroups).length} view groups that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const objectMetadataIdsUsedInViews = views.map(
|
||||
(view) => view.objectMetadataId,
|
||||
);
|
||||
|
||||
const objectMetadataRepository =
|
||||
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||
const objectMetadataItems = await objectMetadataRepository.find({
|
||||
where: {
|
||||
id: In(objectMetadataIdsUsedInViews),
|
||||
},
|
||||
});
|
||||
|
||||
const existingObjectMetadataIds = objectMetadataItems.map(
|
||||
(objectMetadata) => objectMetadata.id,
|
||||
);
|
||||
|
||||
const corruptedViewIds = views
|
||||
.filter(
|
||||
(view) => !existingObjectMetadataIds.includes(view.objectMetadataId),
|
||||
)
|
||||
.map((view) => view.id);
|
||||
|
||||
const workspaceViewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewRepository.delete({
|
||||
id: In(corruptedViewIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewIds.length} out of ${views.length} views that have no corresponding object metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
corruptedViewIds,
|
||||
corruptedViewFieldIds,
|
||||
corruptedViewFilterIds,
|
||||
corruptedViewSortIds,
|
||||
corruptedViewGroupIds,
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteExistingCoreViewObjects(
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewRepository = queryRunner.manager.getRepository(ViewEntity);
|
||||
const existingViews = await viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (existingViews.length === 0) {
|
||||
this.logger.log(
|
||||
`No existing core view objects found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Deleting ${existingViews.length} existing core view objects for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would delete all existing core view objects for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await viewRepository.delete({ workspaceId });
|
||||
|
||||
this.logger.log(
|
||||
`Deleted all existing core view objects for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async migrateViewEntity(
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
let viewName = workspaceView.name;
|
||||
|
||||
if (workspaceView.key === 'INDEX' && !viewName.includes('{')) {
|
||||
viewName = 'All {objectLabelPlural}';
|
||||
}
|
||||
|
||||
const coreView: QueryDeepPartialEntity<ViewEntity> = {
|
||||
id: workspaceView.id,
|
||||
name: viewName,
|
||||
objectMetadataId: workspaceView.objectMetadataId,
|
||||
type: workspaceView.type === 'table' ? ViewType.TABLE : ViewType.KANBAN,
|
||||
key:
|
||||
workspaceView.key === 'INDEX' || workspaceView.key === ViewKey.INDEX
|
||||
? ViewKey.INDEX
|
||||
: null,
|
||||
icon: workspaceView.icon,
|
||||
position: workspaceView.position,
|
||||
isCompact: workspaceView.isCompact,
|
||||
isCustom: workspaceView.key !== 'INDEX',
|
||||
openRecordIn:
|
||||
workspaceView.openRecordIn === 'SIDE_PANEL'
|
||||
? ViewOpenRecordIn.SIDE_PANEL
|
||||
: ViewOpenRecordIn.RECORD_PAGE,
|
||||
kanbanAggregateOperation: workspaceView.kanbanAggregateOperation,
|
||||
kanbanAggregateOperationFieldMetadataId:
|
||||
workspaceView.kanbanAggregateOperationFieldMetadataId,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceView.createdAt),
|
||||
updatedAt: new Date(workspaceView.updatedAt),
|
||||
deletedAt: workspaceView.deletedAt
|
||||
? new Date(workspaceView.deletedAt)
|
||||
: null,
|
||||
anyFieldFilterValue: workspaceView.anyFieldFilterValue,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewEntity);
|
||||
|
||||
await repository.insert(coreView);
|
||||
}
|
||||
|
||||
private async migrateViewFields(
|
||||
workspaceViewFields: ViewFieldWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const field of workspaceViewFields) {
|
||||
const coreViewField: QueryDeepPartialEntity<ViewFieldEntity> = {
|
||||
id: field.id,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
viewId: field.viewId,
|
||||
position: field.position,
|
||||
isVisible: field.isVisible,
|
||||
size: field.size,
|
||||
workspaceId,
|
||||
createdAt: new Date(field.createdAt),
|
||||
updatedAt: new Date(field.updatedAt),
|
||||
deletedAt: field.deletedAt ? new Date(field.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewFieldEntity);
|
||||
|
||||
await repository.insert(coreViewField);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewFilters(
|
||||
workspaceViewFilters: ViewFilterWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const filter of workspaceViewFilters) {
|
||||
if (!filter.viewId) {
|
||||
this.logger.warn(
|
||||
`Skipping view filter ${filter.id} with null viewId for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const coreViewFilter: QueryDeepPartialEntity<ViewFilterEntity> = {
|
||||
id: filter.id,
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
viewId: filter.viewId,
|
||||
operand: convertViewFilterOperandToCoreOperand(
|
||||
filter.operand as SharedViewFilterOperand,
|
||||
),
|
||||
value: convertViewFilterWorkspaceValueToCoreValue(filter.value),
|
||||
viewFilterGroupId: filter.viewFilterGroupId,
|
||||
workspaceId,
|
||||
createdAt: new Date(filter.createdAt),
|
||||
updatedAt: new Date(filter.updatedAt),
|
||||
deletedAt: filter.deletedAt ? new Date(filter.deletedAt) : null,
|
||||
subFieldName: filter.subFieldName,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewFilterEntity);
|
||||
|
||||
await repository.insert(coreViewFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewSorts(
|
||||
workspaceViewSorts: ViewSortWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const sort of workspaceViewSorts) {
|
||||
if (!sort.viewId) {
|
||||
this.logger.warn(
|
||||
`Skipping view sort ${sort.id} with null viewId for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const direction = sort.direction.toUpperCase() as ViewSortDirection;
|
||||
|
||||
const coreViewSort: QueryDeepPartialEntity<ViewSortEntity> = {
|
||||
id: sort.id,
|
||||
fieldMetadataId: sort.fieldMetadataId,
|
||||
viewId: sort.viewId,
|
||||
direction: direction,
|
||||
workspaceId,
|
||||
createdAt: new Date(sort.createdAt),
|
||||
updatedAt: new Date(sort.updatedAt),
|
||||
deletedAt: sort.deletedAt ? new Date(sort.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewSortEntity);
|
||||
|
||||
await repository.insert(coreViewSort);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewGroups(
|
||||
workspaceViewGroups: ViewGroupWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const group of workspaceViewGroups) {
|
||||
if (!group.viewId) {
|
||||
this.logger.warn(
|
||||
`Skipping view group ${group.id} with null viewId for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const coreViewGroup: QueryDeepPartialEntity<ViewGroupEntity> = {
|
||||
id: group.id,
|
||||
fieldMetadataId: group.fieldMetadataId,
|
||||
viewId: group.viewId,
|
||||
fieldValue: group.fieldValue,
|
||||
isVisible: group.isVisible,
|
||||
position: group.position,
|
||||
workspaceId,
|
||||
createdAt: new Date(group.createdAt),
|
||||
updatedAt: new Date(group.updatedAt),
|
||||
deletedAt: group.deletedAt ? new Date(group.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewGroupEntity);
|
||||
|
||||
await repository.insert(coreViewGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewFilterGroups(
|
||||
workspaceViewFilterGroups: ViewFilterGroupWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const filterGroup of workspaceViewFilterGroups.sort(
|
||||
(a, b) =>
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
)) {
|
||||
const coreViewFilterGroup: QueryDeepPartialEntity<ViewFilterGroupEntity> =
|
||||
{
|
||||
id: filterGroup.id,
|
||||
viewId: filterGroup.viewId,
|
||||
logicalOperator:
|
||||
filterGroup.logicalOperator as ViewFilterGroupLogicalOperator,
|
||||
parentViewFilterGroupId: filterGroup.parentViewFilterGroupId,
|
||||
positionInViewFilterGroup: filterGroup.positionInViewFilterGroup,
|
||||
workspaceId,
|
||||
createdAt: new Date(filterGroup.createdAt),
|
||||
updatedAt: new Date(filterGroup.updatedAt),
|
||||
deletedAt: filterGroup.deletedAt
|
||||
? new Date(filterGroup.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(
|
||||
ViewFilterGroupEntity,
|
||||
);
|
||||
|
||||
await repository.insert(coreViewFilterGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
-151
@@ -1,151 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import {
|
||||
FAVORITE_STANDARD_FIELD_IDS,
|
||||
VIEW_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';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-5:remove-favorite-view-relation',
|
||||
description: 'Remove favorite view relation.',
|
||||
})
|
||||
export class RemoveFavoriteViewRelationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
protected readonly workspaceSchemaManager: WorkspaceSchemaManagerService,
|
||||
@InjectDataSource()
|
||||
protected readonly coreDataSource: DataSource,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const objectMetadataRepository =
|
||||
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||
|
||||
const fieldMetadataRepository =
|
||||
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||
|
||||
const [favoriteObjectMetadata] = await objectMetadataRepository.find({
|
||||
where: {
|
||||
standardId: STANDARD_OBJECT_IDS.favorite,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!favoriteObjectMetadata) {
|
||||
throw new Error('Favorite object metadata not found');
|
||||
}
|
||||
|
||||
const [viewObjectMetadata] = await objectMetadataRepository.find({
|
||||
where: {
|
||||
standardId: STANDARD_OBJECT_IDS.view,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!viewObjectMetadata) {
|
||||
throw new Error('View object metadata not found');
|
||||
}
|
||||
|
||||
const [favoriteViewFieldMetadata] = await fieldMetadataRepository.find({
|
||||
where: {
|
||||
objectMetadataId: favoriteObjectMetadata.id,
|
||||
standardId: FAVORITE_STANDARD_FIELD_IDS.view,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const [viewFavoriteFieldMetadata] = await fieldMetadataRepository.find({
|
||||
where: {
|
||||
objectMetadataId: viewObjectMetadata.id,
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.favorites,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!viewFavoriteFieldMetadata || !favoriteViewFieldMetadata) {
|
||||
this.logger.warn(
|
||||
'View or favorite view field metadata not found or already migrated, skipping...',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
await fieldMetadataRepository.delete(viewFavoriteFieldMetadata.id);
|
||||
await fieldMetadataRepository.update(favoriteViewFieldMetadata.id, {
|
||||
name: 'viewId',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'ViewId',
|
||||
description: 'ViewId',
|
||||
icon: 'IconView',
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
relationTargetFieldMetadataId: null,
|
||||
relationTargetObjectMetadataId: null,
|
||||
settings: null,
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceSchemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const foreignKeyName =
|
||||
queryRunner.connection.namingStrategy.foreignKeyName(
|
||||
favoriteObjectMetadata.nameSingular,
|
||||
[`${favoriteViewFieldMetadata.name}Id`],
|
||||
`${workspaceSchemaName}.${viewObjectMetadata.nameSingular}`,
|
||||
[`${viewFavoriteFieldMetadata.name}`],
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceSchemaManager.foreignKeyManager.dropForeignKey({
|
||||
queryRunner,
|
||||
schemaName: workspaceSchemaName,
|
||||
tableName: favoriteObjectMetadata.nameSingular,
|
||||
foreignKeyName,
|
||||
});
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
|
||||
import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command';
|
||||
import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [
|
||||
RemoveFavoriteViewRelationCommand,
|
||||
AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
MigrateViewsToCoreCommand,
|
||||
],
|
||||
exports: [
|
||||
RemoveFavoriteViewRelationCommand,
|
||||
AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
MigrateViewsToCoreCommand,
|
||||
],
|
||||
})
|
||||
export class V1_5_UpgradeVersionCommandModule {}
|
||||
+1
-13
@@ -1,16 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { V0_54_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-54/0-54-upgrade-version-command.module';
|
||||
import { V0_55_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-55/0-55-upgrade-version-command.module';
|
||||
import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-1/1-1-upgrade-version-command.module';
|
||||
import { V1_2_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-2/1-2-upgrade-version-command.module';
|
||||
import { V1_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-3/1-3-upgrade-version-command.module';
|
||||
import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module';
|
||||
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
|
||||
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
|
||||
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
|
||||
import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module';
|
||||
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
@@ -18,12 +12,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
V0_54_UpgradeVersionCommandModule,
|
||||
V0_55_UpgradeVersionCommandModule,
|
||||
V1_1_UpgradeVersionCommandModule,
|
||||
V1_2_UpgradeVersionCommandModule,
|
||||
V1_3_UpgradeVersionCommandModule,
|
||||
V1_5_UpgradeVersionCommandModule,
|
||||
V1_6_UpgradeVersionCommandModule,
|
||||
V1_7_UpgradeVersionCommandModule,
|
||||
V1_8_UpgradeVersionCommandModule,
|
||||
|
||||
-131
@@ -9,29 +9,11 @@ import {
|
||||
UpgradeCommandRunner,
|
||||
type VersionCommands,
|
||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { CleanNotFoundFilesCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-clean-not-found-files.command';
|
||||
import { FixCreatedByDefaultValueCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-created-by-default-value.command';
|
||||
import { FixStandardSelectFieldsPositionCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command';
|
||||
import { LowercaseUserAndInvitationEmailsCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command';
|
||||
import { MigrateDefaultAvatarUrlToUserWorkspaceCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-migrate-default-avatar-url-to-user-workspace.command';
|
||||
import { DeduplicateIndexedFieldsCommand } from 'src/database/commands/upgrade-version-command/0-55/0-55-deduplicate-indexed-fields.command';
|
||||
import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command';
|
||||
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
|
||||
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
|
||||
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
|
||||
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
|
||||
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
|
||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
||||
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
|
||||
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
|
||||
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
|
||||
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
|
||||
import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command';
|
||||
import { UpdateTimestampColumnTypeInWorkspaceSchemaCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-update-timestamp-column-type-in-workspace-schema.command';
|
||||
import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
|
||||
import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command';
|
||||
import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
|
||||
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
|
||||
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
|
||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
||||
@@ -58,36 +40,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
|
||||
// 0.54 Commands
|
||||
protected readonly fixStandardSelectFieldsPositionCommand: FixStandardSelectFieldsPositionCommand,
|
||||
protected readonly fixCreatedByDefaultValueCommand: FixCreatedByDefaultValueCommand,
|
||||
protected readonly cleanNotFoundFilesCommand: CleanNotFoundFilesCommand,
|
||||
protected readonly lowercaseUserAndInvitationEmailsCommand: LowercaseUserAndInvitationEmailsCommand,
|
||||
protected readonly migrateDefaultAvatarUrlToUserWorkspaceCommand: MigrateDefaultAvatarUrlToUserWorkspaceCommand,
|
||||
|
||||
// 0.55 Commands
|
||||
protected readonly deduplicateIndexedFieldsCommand: DeduplicateIndexedFieldsCommand,
|
||||
|
||||
// 1.1 Commands
|
||||
protected readonly fixSchemaArrayTypeCommand: FixSchemaArrayTypeCommand,
|
||||
protected readonly fixUpdateStandardFieldsIsLabelSyncedWithNameCommand: FixUpdateStandardFieldsIsLabelSyncedWithName,
|
||||
protected readonly migrateWorkflowRunStatesCommand: MigrateWorkflowRunStatesCommand,
|
||||
protected readonly addEnqueuedStatusToWorkflowRunCommand: AddEnqueuedStatusToWorkflowRunCommand,
|
||||
|
||||
// 1.2 Commands
|
||||
protected readonly removeWorkflowRunsWithoutState: RemoveWorkflowRunsWithoutState,
|
||||
protected readonly addNextStepIdsToWorkflowVersionTriggers: AddNextStepIdsToWorkflowVersionTriggers,
|
||||
protected readonly addEnqueuedStatusToWorkflowRunV2Command: AddEnqueuedStatusToWorkflowRunV2Command,
|
||||
|
||||
// 1.3 Commands
|
||||
protected readonly addNextStepIdsToWorkflowRunsTrigger: AddNextStepIdsToWorkflowRunsTrigger,
|
||||
protected readonly updateTimestampColumnTypeInWorkspaceSchemaCommand: UpdateTimestampColumnTypeInWorkspaceSchemaCommand,
|
||||
|
||||
// 1.5 Commands
|
||||
protected readonly removeFavoriteViewRelationCommand: RemoveFavoriteViewRelationCommand,
|
||||
protected readonly addPositionsToWorkflowVersionsAndWorkflowRunsCommand: AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
protected readonly migrateViewsToCoreCommand: MigrateViewsToCoreCommand,
|
||||
|
||||
// 1.6 Commands
|
||||
protected readonly fixLabelIdentifierPositionAndVisibilityCommand: FixLabelIdentifierPositionAndVisibilityCommand,
|
||||
|
||||
@@ -115,79 +67,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
syncWorkspaceMetadataCommand,
|
||||
);
|
||||
|
||||
const commands_053: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_054: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.fixStandardSelectFieldsPositionCommand,
|
||||
this.fixCreatedByDefaultValueCommand,
|
||||
],
|
||||
afterSyncMetadata: [
|
||||
this.cleanNotFoundFilesCommand,
|
||||
this.lowercaseUserAndInvitationEmailsCommand,
|
||||
this.migrateDefaultAvatarUrlToUserWorkspaceCommand,
|
||||
],
|
||||
};
|
||||
|
||||
const commands_055: VersionCommands = {
|
||||
beforeSyncMetadata: [this.deduplicateIndexedFieldsCommand],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_060: VersionCommands = {
|
||||
afterSyncMetadata: [],
|
||||
beforeSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_100: VersionCommands = {
|
||||
afterSyncMetadata: [],
|
||||
beforeSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_110: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.fixUpdateStandardFieldsIsLabelSyncedWithNameCommand,
|
||||
this.fixSchemaArrayTypeCommand,
|
||||
this.addEnqueuedStatusToWorkflowRunCommand,
|
||||
],
|
||||
afterSyncMetadata: [this.migrateWorkflowRunStatesCommand],
|
||||
};
|
||||
|
||||
const commands_120: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.removeWorkflowRunsWithoutState,
|
||||
this.addNextStepIdsToWorkflowVersionTriggers,
|
||||
this.addEnqueuedStatusToWorkflowRunV2Command,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_130: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.addNextStepIdsToWorkflowVersionTriggers, // We add that command again because nextStepIds where not added on freshly created triggers. It will be done in 1.3
|
||||
this.addNextStepIdsToWorkflowRunsTrigger,
|
||||
this.updateTimestampColumnTypeInWorkspaceSchemaCommand,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_140: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_150: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.migrateViewsToCoreCommand,
|
||||
this.removeFavoriteViewRelationCommand,
|
||||
this.addPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_160: VersionCommands = {
|
||||
beforeSyncMetadata: [this.fixLabelIdentifierPositionAndVisibilityCommand],
|
||||
afterSyncMetadata: [],
|
||||
@@ -224,16 +103,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
};
|
||||
|
||||
this.allCommands = {
|
||||
'0.53.0': commands_053,
|
||||
'0.54.0': commands_054,
|
||||
'0.55.0': commands_055,
|
||||
'0.60.0': commands_060,
|
||||
'1.0.0': commands_100,
|
||||
'1.1.0': commands_110,
|
||||
'1.2.0': commands_120,
|
||||
'1.3.0': commands_130,
|
||||
'1.4.0': commands_140,
|
||||
'1.5.0': commands_150,
|
||||
'1.6.0': commands_160,
|
||||
'1.7.0': commands_170,
|
||||
'1.8.0': commands_180,
|
||||
|
||||
Reference in New Issue
Block a user