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,
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export enum ViewOpenRecordInType {
|
||||
SIDE_PANEL = 'SIDE_PANEL',
|
||||
RECORD_PAGE = 'RECORD_PAGE',
|
||||
}
|
||||
+1
-1
@@ -11,6 +11,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
|
||||
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 { ViewOpenRecordInType } from 'src/engine/metadata-modules/view/types/view-open-record-in-type.type';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite';
|
||||
import { prefillWorkspaceFavorites } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workspace-favorites';
|
||||
@@ -27,7 +28,6 @@ import { tasksByStatusView } from 'src/engine/workspace-manager/standard-objects
|
||||
import { workflowRunsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view';
|
||||
import { workflowVersionsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view';
|
||||
import { workflowsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view';
|
||||
import { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
type PrefillCoreViewsArgs = {
|
||||
coreDataSource: DataSource;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { type MessageDescriptor } from '@lingui/core';
|
||||
import { type ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
import { type AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { type ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
import { type ViewOpenRecordInType } from 'src/engine/metadata-modules/view/types/view-open-record-in-type.type';
|
||||
|
||||
export interface ViewDefinition {
|
||||
id?: string;
|
||||
|
||||
+1
-1
@@ -1,12 +1,12 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewOpenRecordInType } from 'src/engine/metadata-modules/view/types/view-open-record-in-type.type';
|
||||
import {
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS,
|
||||
DASHBOARD_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 { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
export const dashboardsAllView = (
|
||||
objectMetadataItems: ObjectMetadataEntity[],
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewOpenRecordInType } from 'src/engine/metadata-modules/view/types/view-open-record-in-type.type';
|
||||
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 { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
export const workflowRunsAllView = (
|
||||
objectMetadataItems: ObjectMetadataEntity[],
|
||||
|
||||
+1
-1
@@ -1,12 +1,12 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewOpenRecordInType } from 'src/engine/metadata-modules/view/types/view-open-record-in-type.type';
|
||||
import {
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS,
|
||||
WORKFLOW_VERSION_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 { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
export const workflowVersionsAllView = (
|
||||
objectMetadataItems: ObjectMetadataEntity[],
|
||||
|
||||
+1
-1
@@ -1,12 +1,12 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewOpenRecordInType } from 'src/engine/metadata-modules/view/types/view-open-record-in-type.type';
|
||||
import {
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS,
|
||||
WORKFLOW_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 { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
export const workflowsAllView = (
|
||||
objectMetadataItems: ObjectMetadataEntity[],
|
||||
|
||||
-12
@@ -22,12 +22,6 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
|
||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
|
||||
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
@@ -48,12 +42,6 @@ export const standardObjectMetadataDefinitions = [
|
||||
FavoriteWorkspaceEntity,
|
||||
FavoriteFolderWorkspaceEntity,
|
||||
TimelineActivityWorkspaceEntity,
|
||||
ViewFieldWorkspaceEntity,
|
||||
ViewGroupWorkspaceEntity,
|
||||
ViewFilterWorkspaceEntity,
|
||||
ViewFilterGroupWorkspaceEntity,
|
||||
ViewSortWorkspaceEntity,
|
||||
ViewWorkspaceEntity,
|
||||
WorkflowWorkspaceEntity,
|
||||
WorkflowVersionWorkspaceEntity,
|
||||
WorkflowRunWorkspaceEntity,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ConnectedAccountModule } from 'src/modules/connected-account/connected-
|
||||
import { FavoriteFolderModule } from 'src/modules/favorite-folder/favorite-folder.module';
|
||||
import { FavoriteModule } from 'src/modules/favorite/favorite.module';
|
||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||
import { StandardViewModule } from 'src/modules/view/standard-view.module';
|
||||
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
|
||||
@Module({
|
||||
@@ -13,7 +12,6 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
MessagingModule,
|
||||
CalendarModule,
|
||||
ConnectedAccountModule,
|
||||
StandardViewModule,
|
||||
WorkflowModule,
|
||||
FavoriteFolderModule,
|
||||
FavoriteModule,
|
||||
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { Relation } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_FIELD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
registerEnumType(AggregateOperations, {
|
||||
name: 'AggregateOperations',
|
||||
});
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewField,
|
||||
namePlural: 'viewFields',
|
||||
labelSingular: msg`View Field`,
|
||||
labelPlural: msg`View Fields`,
|
||||
description: msg`(System) View Fields`,
|
||||
icon: STANDARD_OBJECT_ICONS.viewField,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
})
|
||||
export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Field Metadata Id`,
|
||||
description: msg`View Field target field`,
|
||||
icon: 'IconTag',
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.isVisible,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: msg`Visible`,
|
||||
description: msg`View Field visibility`,
|
||||
icon: 'IconEye',
|
||||
defaultValue: true,
|
||||
})
|
||||
isVisible: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.size,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: msg`Size`,
|
||||
description: msg`View Field size`,
|
||||
icon: 'IconEye',
|
||||
defaultValue: 0,
|
||||
})
|
||||
size: number;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.position,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: msg`Position`,
|
||||
description: msg`View Field position`,
|
||||
icon: 'IconList',
|
||||
defaultValue: 0,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
position: number;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.view,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`View`,
|
||||
description: msg`View Field related view`,
|
||||
icon: 'IconLayoutCollage',
|
||||
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||
inverseSideFieldKey: 'viewFields',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
view: Relation<ViewWorkspaceEntity>;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.aggregateOperation,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: msg`Aggregate operation`,
|
||||
description: msg`Optional aggregate operation`,
|
||||
icon: 'IconCalculator',
|
||||
options: [
|
||||
{
|
||||
value: AggregateOperations.AVG,
|
||||
label: 'Average',
|
||||
position: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT,
|
||||
label: 'Count',
|
||||
position: 1,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.MAX,
|
||||
label: 'Maximum',
|
||||
position: 2,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.MIN,
|
||||
label: 'Minimum',
|
||||
position: 3,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.SUM,
|
||||
label: 'Sum',
|
||||
position: 4,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_EMPTY,
|
||||
label: 'Count empty',
|
||||
position: 5,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_NOT_EMPTY,
|
||||
label: 'Count not empty',
|
||||
position: 6,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_UNIQUE_VALUES,
|
||||
label: 'Count unique values',
|
||||
position: 7,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.PERCENTAGE_EMPTY,
|
||||
label: 'Percent empty',
|
||||
position: 8,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.PERCENTAGE_NOT_EMPTY,
|
||||
label: 'Percent not empty',
|
||||
position: 9,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_TRUE,
|
||||
label: 'Count true',
|
||||
position: 10,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_FALSE,
|
||||
label: 'Count false',
|
||||
position: 11,
|
||||
color: 'purple',
|
||||
},
|
||||
],
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
aggregateOperation?: AggregateOperations | null;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string;
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { Relation } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_FILTER_GROUP_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 { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
export enum ViewFilterGroupLogicalOperator {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
NOT = 'NOT',
|
||||
}
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewFilterGroup,
|
||||
namePlural: 'viewFilterGroups',
|
||||
labelSingular: msg`View Filter Group`,
|
||||
labelPlural: msg`View Filter Groups`,
|
||||
description: msg`(System) View Filter Groups`,
|
||||
icon: 'IconFilterBolt',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
export class ViewFilterGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.view,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`View`,
|
||||
description: msg`View`,
|
||||
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||
inverseSideFieldKey: 'viewFilterGroups',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
view: Relation<ViewWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.parentViewFilterGroupId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Parent View Filter Group Id`,
|
||||
description: msg`Parent View Filter Group`,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
parentViewFilterGroupId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.logicalOperator,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: msg`Logical Operator`,
|
||||
description: msg`Logical operator for the filter group`,
|
||||
options: [
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.AND,
|
||||
label: 'AND',
|
||||
position: 0,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.OR,
|
||||
label: 'OR',
|
||||
position: 1,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.NOT,
|
||||
label: 'NOT',
|
||||
position: 2,
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${ViewFilterGroupLogicalOperator.NOT}'`,
|
||||
})
|
||||
logicalOperator: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.positionInViewFilterGroup,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: msg`Position in view filter group`,
|
||||
description: msg`Position in the parent view filter group`,
|
||||
icon: 'IconHierarchy2',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
positionInViewFilterGroup: number | null;
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { type Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewFilter,
|
||||
namePlural: 'viewFilters',
|
||||
labelSingular: msg`View Filter`,
|
||||
labelPlural: msg`View Filters`,
|
||||
description: msg`(System) View Filters`,
|
||||
icon: STANDARD_OBJECT_ICONS.viewFilter,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Field Metadata Id`,
|
||||
description: msg`View Filter target field`,
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.operand,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Operand`,
|
||||
description: msg`View Filter operand`,
|
||||
defaultValue: "'Contains'",
|
||||
})
|
||||
operand: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.value,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Value`,
|
||||
description: msg`View Filter value`,
|
||||
})
|
||||
value: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.displayValue,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Display Value`,
|
||||
description: msg`View Filter Display Value`,
|
||||
})
|
||||
displayValue: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.view,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`View`,
|
||||
description: msg`View Filter related view`,
|
||||
icon: 'IconLayoutCollage',
|
||||
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||
inverseSideFieldKey: 'viewFilters',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
view: Relation<ViewWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.viewFilterGroupId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`View Filter Group Id`,
|
||||
description: msg`View Filter Group`,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewFilterGroupId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.positionInViewFilterGroup,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: msg`Position in view filter group`,
|
||||
description: msg`Position in the view filter group`,
|
||||
icon: 'IconHierarchy2',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
positionInViewFilterGroup: number | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.subFieldName,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Sub field name`,
|
||||
description: msg`Sub field name`,
|
||||
icon: 'IconSubtask',
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
subFieldName: string | null;
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewGroup,
|
||||
namePlural: 'viewGroups',
|
||||
labelSingular: msg`View Group`,
|
||||
labelPlural: msg`View Groups`,
|
||||
description: msg`(System) View Groups`,
|
||||
icon: STANDARD_OBJECT_ICONS.viewGroup,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Field Metadata Id`,
|
||||
description: msg`View Group target field`,
|
||||
icon: 'IconTag',
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.isVisible,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: msg`Visible`,
|
||||
description: msg`View Group visibility`,
|
||||
icon: 'IconEye',
|
||||
defaultValue: true,
|
||||
})
|
||||
isVisible: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.fieldValue,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Field Value`,
|
||||
description: msg`Group by this field value`,
|
||||
})
|
||||
fieldValue: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.position,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: msg`Position`,
|
||||
description: msg`View Field position`,
|
||||
icon: 'IconList',
|
||||
defaultValue: 0,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
position: number;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.view,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`View`,
|
||||
description: msg`View Group related view`,
|
||||
icon: 'IconLayoutCollage',
|
||||
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||
inverseSideFieldKey: 'viewGroups',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
view?: ViewWorkspaceEntity | null;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string | null;
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { type Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_SORT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewSort,
|
||||
namePlural: 'viewSorts',
|
||||
labelSingular: msg`View Sort`,
|
||||
labelPlural: msg`View Sorts`,
|
||||
description: msg`(System) View Sorts`,
|
||||
icon: STANDARD_OBJECT_ICONS.viewSort,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
})
|
||||
export class ViewSortWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_SORT_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Field Metadata Id`,
|
||||
description: msg`View Sort target field`,
|
||||
icon: 'IconTag',
|
||||
})
|
||||
fieldMetadataId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_SORT_STANDARD_FIELD_IDS.direction,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Direction`,
|
||||
description: msg`View Sort direction`,
|
||||
defaultValue: "'asc'",
|
||||
})
|
||||
direction: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_SORT_STANDARD_FIELD_IDS.view,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`View`,
|
||||
description: msg`View Sort related view`,
|
||||
icon: 'IconLayoutCollage',
|
||||
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||
inverseSideFieldKey: 'viewSorts',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
view: Relation<ViewWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string | null;
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
|
||||
export enum ViewOpenRecordInType {
|
||||
SIDE_PANEL = 'SIDE_PANEL',
|
||||
RECORD_PAGE = 'RECORD_PAGE',
|
||||
}
|
||||
|
||||
registerEnumType(ViewOpenRecordInType, {
|
||||
name: 'ViewOpenRecordInType',
|
||||
});
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.view,
|
||||
namePlural: 'views',
|
||||
labelSingular: msg`View`,
|
||||
labelPlural: msg`Views`,
|
||||
description: msg`(System) Views`,
|
||||
icon: STANDARD_OBJECT_ICONS.view,
|
||||
labelIdentifierStandardId: VIEW_STANDARD_FIELD_IDS.name,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Name`,
|
||||
description: msg`View name`,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.objectMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Object Metadata Id`,
|
||||
description: msg`View target object`,
|
||||
})
|
||||
objectMetadataId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.type,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Type`,
|
||||
description: msg`View type`,
|
||||
defaultValue: "'table'",
|
||||
})
|
||||
type: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.key,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: msg`Key`,
|
||||
description: msg`View key`,
|
||||
options: [{ value: 'INDEX', label: 'Index', position: 0, color: 'red' }],
|
||||
defaultValue: "'INDEX'",
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
key: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.icon,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Icon`,
|
||||
description: msg`View icon`,
|
||||
})
|
||||
icon: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.kanbanFieldMetadataId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`kanbanfieldMetadataId`,
|
||||
description: msg`View Kanban column field`,
|
||||
})
|
||||
/**
|
||||
* @deprecated Use `viewGroups.fieldMetadataId` instead
|
||||
*/
|
||||
kanbanFieldMetadataId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.position,
|
||||
type: FieldMetadataType.POSITION,
|
||||
label: msg`Position`,
|
||||
description: msg`View position`,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
position: number;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.isCompact,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: msg`Compact View`,
|
||||
description: msg`Describes if the view is in compact mode`,
|
||||
defaultValue: false,
|
||||
})
|
||||
isCompact: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.openRecordIn,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: msg`Open Record In`,
|
||||
description: msg`Display the records in a side panel or in a record page`,
|
||||
defaultValue: `'${ViewOpenRecordInType.SIDE_PANEL}'`,
|
||||
options: [
|
||||
{
|
||||
value: ViewOpenRecordInType.SIDE_PANEL,
|
||||
label: 'Side Panel',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: ViewOpenRecordInType.RECORD_PAGE,
|
||||
label: 'Record Page',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
})
|
||||
openRecordIn: ViewOpenRecordInType;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewFields,
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: msg`View Fields`,
|
||||
description: msg`View Fields`,
|
||||
icon: 'IconTag',
|
||||
inverseSideTarget: () => ViewFieldWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewFields: Relation<ViewFieldWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewGroups,
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: msg`View Groups`,
|
||||
description: msg`View Groups`,
|
||||
icon: 'IconTag',
|
||||
inverseSideTarget: () => ViewGroupWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewGroups: Relation<ViewGroupWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewFilters,
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: msg`View Filters`,
|
||||
description: msg`View Filters`,
|
||||
icon: 'IconFilterBolt',
|
||||
inverseSideTarget: () => ViewFilterWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewFilters: Relation<ViewFilterWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewFilterGroups,
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: msg`View Filter Groups`,
|
||||
description: msg`View Filter Groups`,
|
||||
icon: 'IconFilterBolt',
|
||||
inverseSideTarget: () => ViewFilterGroupWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewFilterGroups: Relation<ViewFilterGroupWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewSorts,
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: msg`View Sorts`,
|
||||
description: msg`View Sorts`,
|
||||
icon: 'IconArrowsSort',
|
||||
inverseSideTarget: () => ViewSortWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewSorts: Relation<ViewSortWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: msg`Aggregate operation`,
|
||||
description: msg`Optional aggregate operation`,
|
||||
icon: 'IconCalculator',
|
||||
options: [
|
||||
{
|
||||
value: AggregateOperations.AVG,
|
||||
label: 'Average',
|
||||
position: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT,
|
||||
label: 'Count',
|
||||
position: 1,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.MAX,
|
||||
label: 'Maximum',
|
||||
position: 2,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.MIN,
|
||||
label: 'Minimum',
|
||||
position: 3,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.SUM,
|
||||
label: 'Sum',
|
||||
position: 4,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_EMPTY,
|
||||
label: 'Count empty',
|
||||
position: 5,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_NOT_EMPTY,
|
||||
label: 'Count not empty',
|
||||
position: 6,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_UNIQUE_VALUES,
|
||||
label: 'Count unique values',
|
||||
position: 7,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.PERCENTAGE_EMPTY,
|
||||
label: 'Percent empty',
|
||||
position: 8,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.PERCENTAGE_NOT_EMPTY,
|
||||
label: 'Percent not empty',
|
||||
position: 9,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_TRUE,
|
||||
label: 'Count true',
|
||||
position: 10,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AggregateOperations.COUNT_FALSE,
|
||||
label: 'Count false',
|
||||
position: 11,
|
||||
color: 'purple',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${AggregateOperations.COUNT}'`,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
kanbanAggregateOperation?: AggregateOperations | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperationFieldMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: msg`Field metadata used for aggregate operation`,
|
||||
description: msg`Field metadata used for aggregate operation`,
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
kanbanAggregateOperationFieldMetadataId?: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.anyFieldFilterValue,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Any field filter value`,
|
||||
description: msg`Any field filter value`,
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
anyFieldFilterValue?: string | null;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
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 { 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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
ViewEntity,
|
||||
ViewFieldEntity,
|
||||
ViewFilterEntity,
|
||||
ViewFilterGroupEntity,
|
||||
ViewGroupEntity,
|
||||
ViewSortEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity]),
|
||||
FeatureFlagModule,
|
||||
],
|
||||
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class StandardViewModule {}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type';
|
||||
|
||||
export const convertViewFilterWorkspaceValueToCoreValue = (
|
||||
value: string,
|
||||
): ViewFilterValue => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class ViewException extends CustomException<ViewExceptionCode> {}
|
||||
|
||||
export enum ViewExceptionCode {
|
||||
CORE_VIEW_SYNC_ERROR = 'CORE_VIEW_SYNC_ERROR',
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { ViewFilterGroupLogicalOperator } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator';
|
||||
|
||||
describe('group-by resolver (integration)', () => {
|
||||
describe('standard case', () => {
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import request from 'supertest';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('viewFieldsResolver (e2e)', () => {
|
||||
it('should find many viewFields', () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
query viewFields {
|
||||
viewFields {
|
||||
edges {
|
||||
node {
|
||||
fieldMetadataId
|
||||
isVisible
|
||||
size
|
||||
position
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
viewId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.errors).toBeUndefined();
|
||||
})
|
||||
.expect((res) => {
|
||||
const data = res.body.data.viewFields;
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(Array.isArray(data.edges)).toBe(true);
|
||||
|
||||
const edges = data.edges;
|
||||
|
||||
if (edges.length > 0) {
|
||||
const viewFields = edges[0].node;
|
||||
|
||||
expect(viewFields).toHaveProperty('fieldMetadataId');
|
||||
expect(viewFields).toHaveProperty('isVisible');
|
||||
expect(viewFields).toHaveProperty('size');
|
||||
expect(viewFields).toHaveProperty('position');
|
||||
expect(viewFields).toHaveProperty('id');
|
||||
expect(viewFields).toHaveProperty('createdAt');
|
||||
expect(viewFields).toHaveProperty('updatedAt');
|
||||
expect(viewFields).toHaveProperty('deletedAt');
|
||||
expect(viewFields).toHaveProperty('viewId');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import request from 'supertest';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('viewFiltersResolver (e2e)', () => {
|
||||
it('should find many viewFilters', () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
query viewFilters {
|
||||
viewFilters {
|
||||
edges {
|
||||
node {
|
||||
fieldMetadataId
|
||||
operand
|
||||
value
|
||||
displayValue
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
viewId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.errors).toBeUndefined();
|
||||
})
|
||||
.expect((res) => {
|
||||
const data = res.body.data.viewFilters;
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(Array.isArray(data.edges)).toBe(true);
|
||||
|
||||
const edges = data.edges;
|
||||
|
||||
if (edges.length > 0) {
|
||||
const viewFilters = edges[0].node;
|
||||
|
||||
expect(viewFilters).toHaveProperty('fieldMetadataId');
|
||||
expect(viewFilters).toHaveProperty('operand');
|
||||
expect(viewFilters).toHaveProperty('value');
|
||||
expect(viewFilters).toHaveProperty('displayValue');
|
||||
expect(viewFilters).toHaveProperty('id');
|
||||
expect(viewFilters).toHaveProperty('createdAt');
|
||||
expect(viewFilters).toHaveProperty('updatedAt');
|
||||
expect(viewFilters).toHaveProperty('deletedAt');
|
||||
expect(viewFilters).toHaveProperty('viewId');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import request from 'supertest';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('viewSortsResolver (e2e)', () => {
|
||||
it('should find many viewSorts', () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
query viewSorts {
|
||||
viewSorts {
|
||||
edges {
|
||||
node {
|
||||
fieldMetadataId
|
||||
direction
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
viewId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.errors).toBeUndefined();
|
||||
})
|
||||
.expect((res) => {
|
||||
const data = res.body.data.viewSorts;
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(Array.isArray(data.edges)).toBe(true);
|
||||
|
||||
const edges = data.edges;
|
||||
|
||||
if (edges.length > 0) {
|
||||
const viewSorts = edges[0].node;
|
||||
|
||||
expect(viewSorts).toHaveProperty('fieldMetadataId');
|
||||
expect(viewSorts).toHaveProperty('direction');
|
||||
expect(viewSorts).toHaveProperty('id');
|
||||
expect(viewSorts).toHaveProperty('createdAt');
|
||||
expect(viewSorts).toHaveProperty('updatedAt');
|
||||
expect(viewSorts).toHaveProperty('deletedAt');
|
||||
expect(viewSorts).toHaveProperty('viewId');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
import request from 'supertest';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('viewsResolver (e2e)', () => {
|
||||
it('should find many views', () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
query views {
|
||||
views {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
objectMetadataId
|
||||
type
|
||||
key
|
||||
icon
|
||||
kanbanFieldMetadataId
|
||||
position
|
||||
isCompact
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.errors).toBeUndefined();
|
||||
})
|
||||
.expect((res) => {
|
||||
const data = res.body.data.views;
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(Array.isArray(data.edges)).toBe(true);
|
||||
|
||||
const edges = data.edges;
|
||||
|
||||
if (edges.length > 0) {
|
||||
const views = edges[0].node;
|
||||
|
||||
expect(views).toHaveProperty('name');
|
||||
expect(views).toHaveProperty('objectMetadataId');
|
||||
expect(views).toHaveProperty('type');
|
||||
expect(views).toHaveProperty('key');
|
||||
expect(views).toHaveProperty('icon');
|
||||
expect(views).toHaveProperty('kanbanFieldMetadataId');
|
||||
expect(views).toHaveProperty('position');
|
||||
expect(views).toHaveProperty('isCompact');
|
||||
expect(views).toHaveProperty('id');
|
||||
expect(views).toHaveProperty('createdAt');
|
||||
expect(views).toHaveProperty('updatedAt');
|
||||
expect(views).toHaveProperty('deletedAt');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`successful find view with all sub-relations (e2e) Company View Structure Validation should successfully fetch complete view structure with all sub-relations in single GraphQL query 1`] = `
|
||||
{
|
||||
"anyFieldFilterValue": null,
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"icon": "IconList",
|
||||
"id": Any<String>,
|
||||
"isCompact": false,
|
||||
"key": "INDEX",
|
||||
"name": "All Companies",
|
||||
"objectMetadataId": Any<String>,
|
||||
"openRecordIn": "SIDE_PANEL",
|
||||
"position": 0,
|
||||
"type": "TABLE",
|
||||
"updatedAt": Any<String>,
|
||||
"viewFields": [
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 0,
|
||||
"size": 180,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 1,
|
||||
"size": 100,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 2,
|
||||
"size": 150,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 3,
|
||||
"size": 150,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 4,
|
||||
"size": 150,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 5,
|
||||
"size": 150,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 6,
|
||||
"size": 170,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"deletedAt": null,
|
||||
"fieldMetadataId": Any<String>,
|
||||
"id": Any<String>,
|
||||
"isVisible": true,
|
||||
"position": 7,
|
||||
"size": 170,
|
||||
"updatedAt": Any<String>,
|
||||
"viewId": Any<String>,
|
||||
},
|
||||
],
|
||||
"viewFilterGroups": [],
|
||||
"viewFilters": [],
|
||||
"viewGroups": [],
|
||||
"viewSorts": [],
|
||||
"workspaceId": Any<String>,
|
||||
}
|
||||
`;
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
VIEW_FIELD_GQL_FIELDS,
|
||||
VIEW_FILTER_GQL_FIELDS,
|
||||
VIEW_FILTER_GROUP_GQL_FIELDS,
|
||||
VIEW_GQL_FIELDS,
|
||||
VIEW_GROUP_GQL_FIELDS,
|
||||
VIEW_SORT_GQL_FIELDS,
|
||||
} from 'test/integration/constants/view-gql-fields.constants';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { findCoreViews } from 'test/integration/metadata/suites/view/utils/find-core-views.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
|
||||
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
|
||||
|
||||
// TODO prastoin deprecate cleanupViewRecords that breaks this test suite
|
||||
describe.skip('successful find view with all sub-relations (e2e)', () => {
|
||||
let companyObjectMetadataId: string;
|
||||
|
||||
const COMPREHENSIVE_VIEW_GQL_FIELDS = `
|
||||
${VIEW_GQL_FIELDS}
|
||||
viewFields {
|
||||
${VIEW_FIELD_GQL_FIELDS}
|
||||
}
|
||||
viewFilters {
|
||||
${VIEW_FILTER_GQL_FIELDS}
|
||||
}
|
||||
viewGroups {
|
||||
${VIEW_GROUP_GQL_FIELDS}
|
||||
}
|
||||
viewSorts {
|
||||
${VIEW_SORT_GQL_FIELDS}
|
||||
}
|
||||
viewFilterGroups {
|
||||
${VIEW_FILTER_GROUP_GQL_FIELDS}
|
||||
}
|
||||
`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: {
|
||||
first: 1000,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
`,
|
||||
});
|
||||
|
||||
const companyObject = objects.find((obj) => obj.nameSingular === 'company');
|
||||
|
||||
jestExpectToBeDefined(companyObject);
|
||||
companyObjectMetadataId = companyObject.id;
|
||||
});
|
||||
|
||||
describe('Company View Structure Validation', () => {
|
||||
it('should successfully fetch complete view structure with all sub-relations in single GraphQL query', async () => {
|
||||
const { data: viewsData } = await findCoreViews({
|
||||
objectMetadataId: companyObjectMetadataId,
|
||||
gqlFields: COMPREHENSIVE_VIEW_GQL_FIELDS,
|
||||
expectToFail: false,
|
||||
});
|
||||
const testView = viewsData.getCoreViews.find(
|
||||
(view) => view.key === ViewKey.INDEX,
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(testView);
|
||||
|
||||
expect(testView).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny({ ...testView }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator';
|
||||
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewFilterGroupLogicalOperator } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
|
||||
export const cleanupViewRecords = async (): Promise<void> => {
|
||||
await global.testDataSource.query(`DELETE from "core"."view"`);
|
||||
|
||||
Reference in New Issue
Block a user