[FIXES_MAIN] Remove objectMetadata standardId (#17632)
# Introduction In this PR we're deprecating the object metadata standard id and replacing it to the universalIdentifier usage As we've totally removed its insertion for both new field and object in https://github.com/twentyhq/twenty/pull/17572 ## Note - Removed upgrade commands before `1.17`
This commit is contained in:
-183
@@ -1,183 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:backfill-page-layout-universal-identifiers',
|
||||
description:
|
||||
'Backfill universalIdentifier and applicationId for pageLayout, pageLayoutTab and pageLayoutWidget',
|
||||
})
|
||||
export class BackfillPageLayoutUniversalIdentifiersCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(PageLayoutWidgetEntity)
|
||||
private readonly pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>,
|
||||
@InjectRepository(PageLayoutTabEntity)
|
||||
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
|
||||
@InjectRepository(PageLayoutEntity)
|
||||
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting backfill of universalIdentifier and applicationId for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const applicationId = workspaceCustomFlatApplication.id;
|
||||
|
||||
const pageLayoutsToUpdate = await this.pageLayoutRepository.find({
|
||||
where: [
|
||||
{ workspaceId, universalIdentifier: IsNull() },
|
||||
{ workspaceId, applicationId: IsNull() },
|
||||
],
|
||||
select: ['id', 'workspaceId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${pageLayoutsToUpdate.length} pageLayout records to backfill`,
|
||||
);
|
||||
|
||||
for (const pageLayout of pageLayoutsToUpdate) {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update pageLayout ${pageLayout.id} with universalIdentifier ${universalIdentifier} and applicationId ${applicationId}`,
|
||||
);
|
||||
} else {
|
||||
await this.pageLayoutRepository.update(
|
||||
{ id: pageLayout.id },
|
||||
{
|
||||
universalIdentifier,
|
||||
applicationId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Updated pageLayout ${pageLayout.id} with universalIdentifier ${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
const tabsToUpdate = await this.pageLayoutTabRepository.find({
|
||||
where: [
|
||||
{ workspaceId, universalIdentifier: IsNull() },
|
||||
{ workspaceId, applicationId: IsNull() },
|
||||
],
|
||||
select: ['id', 'workspaceId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${tabsToUpdate.length} pageLayoutTab records to backfill`,
|
||||
);
|
||||
|
||||
for (const tab of tabsToUpdate) {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update pageLayoutTab ${tab.id} with universalIdentifier ${universalIdentifier} and applicationId ${applicationId}`,
|
||||
);
|
||||
} else {
|
||||
await this.pageLayoutTabRepository.update(
|
||||
{ id: tab.id },
|
||||
{
|
||||
universalIdentifier,
|
||||
applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Updated pageLayoutTab ${tab.id} with universalIdentifier ${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const widgetsToUpdate = await this.pageLayoutWidgetRepository.find({
|
||||
where: [
|
||||
{ workspaceId, universalIdentifier: IsNull() },
|
||||
{ workspaceId, applicationId: IsNull() },
|
||||
],
|
||||
select: ['id', 'workspaceId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${widgetsToUpdate.length} pageLayoutWidget records to backfill`,
|
||||
);
|
||||
|
||||
for (const widget of widgetsToUpdate) {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update pageLayoutWidget ${widget.id} with universalIdentifier ${universalIdentifier} and applicationId ${applicationId}`,
|
||||
);
|
||||
} else {
|
||||
await this.pageLayoutWidgetRepository.update(
|
||||
{ id: widget.id },
|
||||
{
|
||||
universalIdentifier,
|
||||
applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Updated pageLayoutWidget ${widget.id} with universalIdentifier ${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${options.dryRun ? '[DRY RUN] Would have ' : ''}Successfully backfilled ${pageLayoutsToUpdate.length} pageLayouts, ${tabsToUpdate.length} pageLayoutTabs and ${widgetsToUpdate.length} pageLayoutWidgets`,
|
||||
);
|
||||
|
||||
if (
|
||||
!options.dryRun &&
|
||||
(pageLayoutsToUpdate.length > 0 ||
|
||||
tabsToUpdate.length > 0 ||
|
||||
widgetsToUpdate.length > 0)
|
||||
) {
|
||||
this.logger.log(
|
||||
`Invalidating and recomputing cache for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
]);
|
||||
|
||||
this.logger.log(`Cache invalidated and recomputed successfully`);
|
||||
}
|
||||
}
|
||||
}
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.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-13:clean-empty-string-null-in-text-fields',
|
||||
description:
|
||||
'Clean up empty string defaults in TEXT fields and convert them to NULL',
|
||||
})
|
||||
export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(
|
||||
CleanEmptyStringNullInTextFieldsCommand.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
protected readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
dataSource,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
if (!isDefined(dataSource)) {
|
||||
throw new Error(
|
||||
`Could not find data source for workspace ${workspaceId}, should never occur`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
});
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
throw new Error(
|
||||
`Could not find workspace ${workspaceId}, should never occur`,
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log('Dry run mode: No changes will be applied');
|
||||
}
|
||||
|
||||
const objectMetadataItems = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
for (const objectMetadataItem of objectMetadataItems) {
|
||||
try {
|
||||
const tableName = computeObjectTargetTable(objectMetadataItem);
|
||||
|
||||
if (!objectMetadataItem.isCustom) {
|
||||
await this.cleanUpEmptyStringDefaultsInTextFieldsInStandardObjects(
|
||||
objectMetadataItem,
|
||||
tableName,
|
||||
schemaName,
|
||||
dataSource,
|
||||
isDryRun,
|
||||
);
|
||||
}
|
||||
|
||||
if (objectMetadataItem.isCustom) {
|
||||
await this.cleanUpEmptyStringDefaultsAndSetNullableInNameFieldInCustomObjects(
|
||||
objectMetadataItem,
|
||||
tableName,
|
||||
schemaName,
|
||||
dataSource,
|
||||
isDryRun,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not cleanup ${objectMetadataItem.isCustom ? 'custom' : 'standard'} object ${objectMetadataItem.nameSingular} records for workspace ${workspaceId}`,
|
||||
);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanUpEmptyStringDefaultsInTextFieldsInStandardObjects(
|
||||
objectMetadataItem: ObjectMetadataEntity,
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
dataSource: GlobalWorkspaceDataSource,
|
||||
isDryRun: boolean,
|
||||
): Promise<void> {
|
||||
const textFields = objectMetadataItem.fields.filter(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.TEXT &&
|
||||
field.isNullable === true &&
|
||||
field.defaultValue === null,
|
||||
);
|
||||
|
||||
for (const field of textFields) {
|
||||
this.logger.log(
|
||||
`Checking field ${field.name} on standard object ${objectMetadataItem.nameSingular} (Table: ${tableName})`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Cleaning up empty string default for field ${field.name} on ${tableName}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
await dataSource.query(
|
||||
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${field.name}" DROP DEFAULT`,
|
||||
[],
|
||||
undefined,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await dataSource.query(
|
||||
`UPDATE "${schemaName}"."${tableName}" SET "${field.name}" = NULL WHERE "${field.name}" = ''`,
|
||||
[],
|
||||
undefined,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanUpEmptyStringDefaultsAndSetNullableInNameFieldInCustomObjects(
|
||||
objectMetadataItem: ObjectMetadataEntity,
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
dataSource: GlobalWorkspaceDataSource,
|
||||
isDryRun: boolean,
|
||||
): Promise<void> {
|
||||
const nameField = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.name === 'name' &&
|
||||
field.type === FieldMetadataType.TEXT &&
|
||||
field.defaultValue === "''",
|
||||
);
|
||||
|
||||
if (!isDefined(nameField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found "name" field with empty string default on ${objectMetadataItem.nameSingular} (Table: ${tableName})`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
await dataSource.query(
|
||||
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "name" DROP DEFAULT, ALTER COLUMN "name" DROP NOT NULL`,
|
||||
[],
|
||||
undefined,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await dataSource.query(
|
||||
`UPDATE "${schemaName}"."${tableName}" SET "name" = NULL WHERE "name" = ''`,
|
||||
[],
|
||||
undefined,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await this.fieldMetadataRepository.update(nameField.id, {
|
||||
defaultValue: null,
|
||||
isNullable: true,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Updated "name" field metadata and cleaned empty strings for ${objectMetadataItem.nameSingular}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
type DuplicateKey = 'apiKeyId' | 'agentId' | 'userWorkspaceId';
|
||||
|
||||
type DuplicateGroup = {
|
||||
foreignKeyId: string;
|
||||
duplicateKey: DuplicateKey;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:deduplicate-role-targets',
|
||||
description:
|
||||
'Remove duplicate roleTargets keeping only the most recently updated one for each unique constraint',
|
||||
})
|
||||
export class DeduplicateRoleTargetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(RoleTargetEntity)
|
||||
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
const duplicateGroups =
|
||||
await this.findDuplicateGroupsForWorkspace(workspaceId);
|
||||
|
||||
if (duplicateGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idsToDelete: string[] = [];
|
||||
|
||||
for (const duplicateGroup of duplicateGroups) {
|
||||
const duplicateIds = await this.findDuplicateIdsToDelete(
|
||||
workspaceId,
|
||||
duplicateGroup,
|
||||
);
|
||||
|
||||
idsToDelete.push(...duplicateIds);
|
||||
}
|
||||
|
||||
if (idsToDelete.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would delete ${idsToDelete.length} duplicate roleTarget(s)`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.roleTargetRepository.delete({ id: In(idsToDelete) });
|
||||
|
||||
this.logger.log(`Deleted ${idsToDelete.length} duplicate roleTarget(s)`);
|
||||
}
|
||||
|
||||
private async findDuplicateGroupsForWorkspace(
|
||||
workspaceId: string,
|
||||
): Promise<DuplicateGroup[]> {
|
||||
const duplicateGroups: DuplicateGroup[] = [];
|
||||
|
||||
const duplicateKeys: DuplicateKey[] = [
|
||||
'apiKeyId',
|
||||
'agentId',
|
||||
'userWorkspaceId',
|
||||
];
|
||||
|
||||
for (const duplicateKey of duplicateKeys) {
|
||||
const duplicates = await this.roleTargetRepository
|
||||
.createQueryBuilder('roleTarget')
|
||||
.select([`roleTarget.${duplicateKey} AS "foreignKeyId"`])
|
||||
.where('roleTarget.workspaceId = :workspaceId', { workspaceId })
|
||||
.andWhere(`roleTarget.${duplicateKey} IS NOT NULL`)
|
||||
.groupBy(`roleTarget.${duplicateKey}`)
|
||||
.having('COUNT(*) > 1')
|
||||
.getRawMany<{ foreignKeyId: string }>();
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
duplicateGroups.push({
|
||||
foreignKeyId: duplicate.foreignKeyId,
|
||||
duplicateKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return duplicateGroups;
|
||||
}
|
||||
|
||||
private async findDuplicateIdsToDelete(
|
||||
workspaceId: string,
|
||||
duplicateGroup: DuplicateGroup,
|
||||
): Promise<string[]> {
|
||||
const { foreignKeyId, duplicateKey } = duplicateGroup;
|
||||
|
||||
const roleTargets = await this.roleTargetRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
[duplicateKey]: foreignKeyId,
|
||||
},
|
||||
order: {
|
||||
updatedAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the first one (most recently updated), delete the rest
|
||||
return roleTargets.slice(1).map((roleTarget) => roleTarget.id);
|
||||
}
|
||||
}
|
||||
-380
@@ -1,380 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultValue,
|
||||
FieldMetadataType,
|
||||
type FromTo,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
|
||||
import {
|
||||
CALENDAR_EVENT_STANDARD_FIELD_IDS,
|
||||
MESSAGE_CHANNEL_STANDARD_FIELD_IDS,
|
||||
MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS,
|
||||
MESSAGE_STANDARD_FIELD_IDS,
|
||||
WORKSPACE_MEMBER_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids';
|
||||
|
||||
const findStandardFlatObjectMetadataOrThrow = ({
|
||||
flatObjectMetadataMaps,
|
||||
standardId,
|
||||
}: {
|
||||
flatObjectMetadataMaps: MetadataFlatEntityMaps<'objectMetadata'>;
|
||||
standardId: string;
|
||||
}): FlatObjectMetadata => {
|
||||
const flatObjectMetadata = Object.values(flatObjectMetadataMaps.byId).find(
|
||||
(flatObjectMetadata) => flatObjectMetadata?.standardId === standardId,
|
||||
);
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
throw new Error(`Could not find standard object ${standardId}`);
|
||||
}
|
||||
|
||||
return flatObjectMetadata;
|
||||
};
|
||||
|
||||
const findStandardFlatFieldMetadatawOrThrow = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
standardId,
|
||||
}: {
|
||||
standardId: string;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: MetadataFlatEntityMaps<'fieldMetadata'>;
|
||||
}) => {
|
||||
const objectFlatFieldMetadata = findManyFlatEntityByIdInFlatEntityMapsOrThrow(
|
||||
{
|
||||
flatEntityIds: flatObjectMetadata.fieldIds,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
const flatFieldMetadata = objectFlatFieldMetadata.find(
|
||||
(flatFieldMetadata) => flatFieldMetadata.standardId === standardId,
|
||||
);
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
throw new Error(`Could not find standard field ${standardId}`);
|
||||
}
|
||||
|
||||
return flatFieldMetadata;
|
||||
};
|
||||
|
||||
const ENUM_FIELD_OPTIONS_TO_MUTATE = {
|
||||
messageChannelMessageAssociation: {
|
||||
fieldStandardId: MESSAGE_STANDARD_FIELD_IDS.direction,
|
||||
field: 'direction',
|
||||
defaultValue: {
|
||||
fromDefaultValue: "'incoming'",
|
||||
toDefaultValue: "'INCOMING'",
|
||||
},
|
||||
options: [
|
||||
{
|
||||
fromOption: {
|
||||
value: 'incoming',
|
||||
label: 'Incoming',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
toOption: {
|
||||
value: 'INCOMING',
|
||||
label: 'Incoming',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
},
|
||||
{
|
||||
fromOption: {
|
||||
value: 'outgoing',
|
||||
label: 'Outgoing',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
toOption: {
|
||||
value: 'OUTGOING',
|
||||
label: 'Outgoing',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
messageChannel: {
|
||||
fieldStandardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.type,
|
||||
field: 'type',
|
||||
defaultValue: { fromDefaultValue: "'email'", toDefaultValue: "'EMAIL'" },
|
||||
options: [
|
||||
{
|
||||
fromOption: {
|
||||
value: 'email',
|
||||
label: 'Email',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
toOption: {
|
||||
value: 'EMAIL',
|
||||
label: 'Email',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
},
|
||||
{
|
||||
fromOption: { value: 'sms', label: 'SMS', position: 1, color: 'blue' },
|
||||
toOption: { value: 'SMS', label: 'SMS', position: 1, color: 'blue' },
|
||||
},
|
||||
],
|
||||
},
|
||||
messageParticipant: {
|
||||
fieldStandardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.role,
|
||||
field: 'role',
|
||||
defaultValue: { fromDefaultValue: "'from'", toDefaultValue: "'FROM'" },
|
||||
options: [
|
||||
{
|
||||
fromOption: {
|
||||
value: 'from',
|
||||
label: 'From',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
toOption: { value: 'FROM', label: 'From', position: 0, color: 'green' },
|
||||
},
|
||||
{
|
||||
fromOption: { value: 'to', label: 'To', position: 1, color: 'blue' },
|
||||
toOption: { value: 'TO', label: 'To', position: 1, color: 'blue' },
|
||||
},
|
||||
{
|
||||
fromOption: { value: 'cc', label: 'Cc', position: 2, color: 'orange' },
|
||||
toOption: { value: 'CC', label: 'Cc', position: 2, color: 'orange' },
|
||||
},
|
||||
{
|
||||
fromOption: { value: 'bcc', label: 'Bcc', position: 3, color: 'red' },
|
||||
toOption: { value: 'BCC', label: 'Bcc', position: 3, color: 'red' },
|
||||
},
|
||||
],
|
||||
},
|
||||
workspaceMember: {
|
||||
fieldStandardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.numberFormat,
|
||||
field: 'numberFormat',
|
||||
defaultValue: { fromDefaultValue: "'system'", toDefaultValue: "'SYSTEM'" },
|
||||
options: [
|
||||
{
|
||||
fromOption: {
|
||||
value: 'SYSTEM',
|
||||
label: 'System',
|
||||
position: 0,
|
||||
color: 'turquoise',
|
||||
},
|
||||
toOption: {
|
||||
value: 'SYSTEM',
|
||||
label: 'System',
|
||||
position: 0,
|
||||
color: 'turquoise',
|
||||
},
|
||||
},
|
||||
{
|
||||
fromOption: {
|
||||
value: 'COMMAS_AND_DOT',
|
||||
label: 'Commas and dot (1,234.56)',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
toOption: {
|
||||
value: 'COMMAS_AND_DOT',
|
||||
label: 'Commas and dot',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
{
|
||||
fromOption: {
|
||||
value: 'SPACES_AND_COMMA',
|
||||
label: 'Spaces and comma (1 234,56)',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
toOption: {
|
||||
value: 'SPACES_AND_COMMA',
|
||||
label: 'Spaces and comma',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
},
|
||||
{
|
||||
fromOption: {
|
||||
value: 'DOTS_AND_COMMA',
|
||||
label: 'Dots and comma (1.234,56)',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
toOption: {
|
||||
value: 'DOTS_AND_COMMA',
|
||||
label: 'Dots and comma',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
},
|
||||
{
|
||||
fromOption: {
|
||||
value: 'APOSTROPHE_AND_DOT',
|
||||
label: "Apostrophe and dot (1'234.56)",
|
||||
position: 4,
|
||||
color: 'purple',
|
||||
},
|
||||
toOption: {
|
||||
value: 'APOSTROPHE_AND_DOT',
|
||||
label: 'Apostrophe and dot',
|
||||
position: 4,
|
||||
color: 'purple',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as const satisfies {
|
||||
[P in AllStandardObjectName]?: {
|
||||
fieldStandardId: string;
|
||||
field: AllStandardObjectFieldName<P>;
|
||||
options: FromTo<FieldMetadataComplexOption, 'option'>[];
|
||||
defaultValue: FromTo<string, 'defaultValue'>;
|
||||
};
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:migrate-standard-invalid-entities',
|
||||
description: 'Migrate standard invalid entities',
|
||||
})
|
||||
export class MigrateStandardInvalidEntitiesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
this.logger.log(
|
||||
`MigrateStandardInvalidEntitiesCommand starting for workspace ${workspaceId}`,
|
||||
);
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const flatCalendarEventObject = findStandardFlatObjectMetadataOrThrow({
|
||||
standardId: STANDARD_OBJECT_IDS.calendarEvent,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const iCalUidFieldId = findStandardFlatFieldMetadatawOrThrow({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata: flatCalendarEventObject,
|
||||
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUid,
|
||||
}).id;
|
||||
|
||||
const allAffectedObjectNames = Object.keys(
|
||||
ENUM_FIELD_OPTIONS_TO_MUTATE,
|
||||
).filter(isDefined) as (keyof typeof ENUM_FIELD_OPTIONS_TO_MUTATE)[];
|
||||
|
||||
const flatEnumFieldMetadatasToUpdate = allAffectedObjectNames.flatMap<{
|
||||
id: string;
|
||||
options: FieldMetadataComplexOption[];
|
||||
defaultValue: FieldMetadataDefaultValue<FieldMetadataType.SELECT>;
|
||||
}>((objectName) => {
|
||||
const { defaultValue, field, options, fieldStandardId } =
|
||||
ENUM_FIELD_OPTIONS_TO_MUTATE[objectName];
|
||||
|
||||
const objectStandardId = STANDARD_OBJECT_IDS[objectName];
|
||||
|
||||
const flatObjectMetadata = findStandardFlatObjectMetadataOrThrow({
|
||||
flatObjectMetadataMaps,
|
||||
standardId: objectStandardId,
|
||||
});
|
||||
|
||||
const flatFieldMetadata = findStandardFlatFieldMetadatawOrThrow({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
standardId: fieldStandardId,
|
||||
});
|
||||
|
||||
if (
|
||||
flatFieldMetadata.name !== field ||
|
||||
!isFlatFieldMetadataOfType(flatFieldMetadata, FieldMetadataType.SELECT)
|
||||
) {
|
||||
throw new Error(
|
||||
`Retrieved flat field is not the one expected expected ${field} got ${flatFieldMetadata.name} in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedOptions = flatFieldMetadata.options.map((option) => {
|
||||
const matchingNewOption = options.find(
|
||||
({ fromOption }) => fromOption.value === option.value,
|
||||
);
|
||||
|
||||
if (!isDefined(matchingNewOption)) {
|
||||
return option;
|
||||
}
|
||||
|
||||
return {
|
||||
...matchingNewOption.toOption,
|
||||
id: option.id,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedDefaultValue: FieldMetadataDefaultValue<FieldMetadataType.SELECT> =
|
||||
flatFieldMetadata.defaultValue === defaultValue.fromDefaultValue
|
||||
? defaultValue.toDefaultValue
|
||||
: flatFieldMetadata.defaultValue;
|
||||
|
||||
return {
|
||||
id: flatFieldMetadata.id,
|
||||
options: updatedOptions,
|
||||
defaultValue: updatedDefaultValue,
|
||||
};
|
||||
});
|
||||
|
||||
const allUpdates = [
|
||||
{
|
||||
id: iCalUidFieldId,
|
||||
name: 'iCalUid',
|
||||
},
|
||||
...flatEnumFieldMetadatasToUpdate,
|
||||
];
|
||||
|
||||
if (!isDryRun) {
|
||||
for (const updateFieldInput of allUpdates) {
|
||||
await this.fieldMetadataService.updateOneField({
|
||||
updateFieldInput,
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.logger.log('Migrated standard invalid entities');
|
||||
}
|
||||
}
|
||||
-267
@@ -1,267 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { TIMELINE_ACTIVITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids';
|
||||
import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:migrate-timeline-activity-to-morph-relations',
|
||||
description:
|
||||
'Migrate timeline activity relations to morph relation fields and set feature flag',
|
||||
})
|
||||
export class MigrateTimelineActivityToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(`Migrating timelineActivity for workspace ${workspaceId}`);
|
||||
|
||||
if (isMigrated) {
|
||||
this.logger.log(
|
||||
`Timeline activity migration already completed. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would have migrated timeline activities for workspace ${workspaceId}. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
const tableName = 'timelineActivity';
|
||||
|
||||
const customObjectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
isCustom: true,
|
||||
},
|
||||
});
|
||||
const customObjectMetadataNames = customObjectMetadata.map(
|
||||
(objectMetadata) => objectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
const fieldNameMigrations = [
|
||||
{ old: 'company', new: 'targetCompany' },
|
||||
{ old: 'person', new: 'targetPerson' },
|
||||
{ old: 'opportunity', new: 'targetOpportunity' },
|
||||
{ old: 'note', new: 'targetNote' },
|
||||
{ old: 'task', new: 'targetTask' },
|
||||
{ old: 'workflow', new: 'targetWorkflow' },
|
||||
{ old: 'workflowVersion', new: 'targetWorkflowVersion' },
|
||||
{ old: 'workflowRun', new: 'targetWorkflowRun' },
|
||||
{ old: 'dashboard', new: 'targetDashboard' },
|
||||
...customObjectMetadataNames.map((customObjectName) => ({
|
||||
old: `${customObjectName}`,
|
||||
new: `target${capitalize(customObjectName)}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const fieldMigrations = fieldNameMigrations.map(
|
||||
({ old: oldFieldName, new: newFieldName }) => ({
|
||||
old: `${oldFieldName}Id`,
|
||||
new: `${newFieldName}Id`,
|
||||
}),
|
||||
);
|
||||
|
||||
const timelineActivityObjectMetadata =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
nameSingular: 'timelineActivity',
|
||||
},
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
if (!timelineActivityObjectMetadata) {
|
||||
this.logger.error(
|
||||
`🟥 Timeline activity object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { new: newField, old: oldField } of fieldMigrations) {
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${schemaName}"."${tableName}"
|
||||
RENAME COLUMN "${oldField}" TO "${newField}"`,
|
||||
);
|
||||
this.logger.log(
|
||||
`Renamed column "${oldField}" to "${newField}" for "${tableName}"`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error renaming column "${oldField}" to "${newField}" for "${tableName}" in workspace ${workspaceId}`,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`✅ Successfully migrated timeline activity records`);
|
||||
|
||||
const objectNamesToMigrate = fieldNameMigrations.map(
|
||||
({ old: oldFieldName }) => oldFieldName,
|
||||
);
|
||||
|
||||
const relatedObjectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const relatedObjectMetadataMap = new Map(
|
||||
relatedObjectMetadata.map((obj) => [obj.nameSingular, obj]),
|
||||
);
|
||||
|
||||
const morphId = TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId;
|
||||
|
||||
for (const objectName of objectNamesToMigrate) {
|
||||
const relatedObject = relatedObjectMetadataMap.get(objectName);
|
||||
|
||||
if (!relatedObject) {
|
||||
this.logger.warn(
|
||||
`Related object metadata not found for ${objectName} in workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldToMigrate = timelineActivityObjectMetadata.fields.find(
|
||||
(field) =>
|
||||
field.type === 'RELATION' &&
|
||||
field.relationTargetObjectMetadataId === relatedObject.id,
|
||||
);
|
||||
|
||||
if (!fieldToMigrate) {
|
||||
this.logger.log(
|
||||
`No RELATION field found for ${objectName} in timelineActivity`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newFieldName = buildTimelineActivityRelatedMorphFieldMetadataName(
|
||||
fieldToMigrate.name,
|
||||
);
|
||||
|
||||
const settings = {
|
||||
...fieldToMigrate.settings,
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: newFieldName,
|
||||
}),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await queryRunner.query(
|
||||
`UPDATE core."fieldMetadata"
|
||||
SET name = $1, type = 'MORPH_RELATION', "morphId" = $3, settings = $4
|
||||
WHERE id = $2`,
|
||||
[newFieldName, fieldToMigrate.id, morphId, settings],
|
||||
);
|
||||
|
||||
const rowsUpdated = result[1] || 0;
|
||||
|
||||
if (rowsUpdated > 0) {
|
||||
this.logger.log(
|
||||
`Updated fieldMetadata: ${fieldToMigrate.name} → ${newFieldName} (type: MORPH_RELATION)`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error updating fieldMetadata for ${objectName} in workspace ${workspaceId}`,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`✅ Successfully migrated timeline activity fieldmetadata`,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
'featureFlagsMap',
|
||||
]);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
this.logger.log(`Cache flushed`);
|
||||
|
||||
this.logger.log(
|
||||
`Set IS_TIMELINE_ACTIVITY_MIGRATED feature flag for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
this.logger.log(`Flush cache for workspace ${workspaceId}`);
|
||||
await this.workspaceCacheStorageService.flush(workspaceId);
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Error migrating timeline activity to morph relations (rolled transaction back on ${workspaceId})`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:rename-index-name',
|
||||
description: 'Rename indexes to use the new v2 index name format',
|
||||
})
|
||||
export class RenameIndexNameCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(RenameIndexNameCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
dataSource,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
if (!isDefined(dataSource)) {
|
||||
throw new Error(
|
||||
`Could not find data source for workspace ${workspaceId}, should never occur`,
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log('Dry run mode: No changes will be applied');
|
||||
}
|
||||
|
||||
const { flatIndexMaps, flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatIndexMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const indexMetadataByName = new Map(
|
||||
Object.values(flatIndexMaps.byId)
|
||||
.filter(isDefined)
|
||||
.map((index) => [index.name, index]),
|
||||
);
|
||||
|
||||
let hasIndexNameChanges = false;
|
||||
let hasRemovedIndexMetadata = false;
|
||||
|
||||
for (const index of Object.values(flatIndexMaps.byId).filter(isDefined)) {
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: index.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const flatFieldMetadatas = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatObjectMetadata.fieldIds,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const { name: indexNameV2 } = generateFlatIndexMetadataWithNameOrThrow({
|
||||
flatObjectMetadata,
|
||||
objectFlatFieldMetadatas: flatFieldMetadatas,
|
||||
flatIndex: index,
|
||||
});
|
||||
|
||||
if (indexNameV2 === index.name) {
|
||||
this.logger.log(`Index ${index.name} is V2`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if another metadata entry already has the v2 name
|
||||
const existingV2Metadata = indexMetadataByName.get(indexNameV2);
|
||||
|
||||
if (isDefined(existingV2Metadata) && existingV2Metadata.id !== index.id) {
|
||||
// V2 metadata already exists, this v1 metadata is stale
|
||||
this.logger.log(
|
||||
`Index metadata with v2 name ${indexNameV2} already exists, removing stale v1 metadata and index ${index.name}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.manager.delete(IndexMetadataEntity, index.id);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "${schemaName}"."${index.name}"`,
|
||||
);
|
||||
await queryRunner.commitTransaction();
|
||||
hasRemovedIndexMetadata = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(`Renaming index ${index.name} to ${indexNameV2}`);
|
||||
|
||||
if (isDryRun) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER INDEX "${schemaName}"."${index.name}" RENAME TO "${indexNameV2}"`,
|
||||
);
|
||||
|
||||
await queryRunner.manager.update(IndexMetadataEntity, index.id, {
|
||||
name: indexNameV2,
|
||||
});
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
hasIndexNameChanges = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
// PostgreSQL error codes for non-existent index:
|
||||
// - 42704: undefined_object
|
||||
// - 42P01: undefined_table (index treated as relation)
|
||||
if (error.code === '42704' || error.code === '42P01') {
|
||||
this.logger.log(
|
||||
`Index ${index.name} does not exist in schema ${schemaName}, removing metadata`,
|
||||
);
|
||||
|
||||
await this.coreDataSource.manager.delete(
|
||||
IndexMetadataEntity,
|
||||
index.id,
|
||||
);
|
||||
hasRemovedIndexMetadata = true;
|
||||
} else if (error.code === '42P07') {
|
||||
// PostgreSQL error code 42P07: duplicate_table (v2 index already exists)
|
||||
// The v2 index already exists, remove stale v1 metadata and index
|
||||
this.logger.log(
|
||||
`Index ${indexNameV2} already exists at PG level, removing stale v1 metadata and index ${index.name}`,
|
||||
);
|
||||
|
||||
await this.coreDataSource.manager.delete(
|
||||
IndexMetadataEntity,
|
||||
index.id,
|
||||
);
|
||||
|
||||
await this.coreDataSource.query(
|
||||
`DROP INDEX IF EXISTS "${schemaName}"."${index.name}"`,
|
||||
);
|
||||
|
||||
hasRemovedIndexMetadata = true;
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Failed to rename index ${index.name}, error code: ${error.code}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
const shouldInvalidateCache =
|
||||
hasIndexNameChanges || hasRemovedIndexMetadata;
|
||||
|
||||
if (shouldInvalidateCache) {
|
||||
this.logger.log('Invalidating workspace cache');
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:update-role-targets-unique-constraint-migration',
|
||||
description:
|
||||
'Update roleTargets unique constraints from combined to separate constraints',
|
||||
})
|
||||
export class UpdateRoleTargetsUniqueConstraintMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.log(
|
||||
'Skipping has already been run once UpdateRoleTargetsUniqueConstraintMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
if (!options.dryRun) {
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."roleTarget" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."roleTarget" ADD CONSTRAINT "IDX_ROLE_TARGET_UNIQUE_API_KEY" UNIQUE ("workspaceId", "apiKeyId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."roleTarget" ADD CONSTRAINT "IDX_ROLE_TARGET_UNIQUE_AGENT" UNIQUE ("workspaceId", "agentId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."roleTarget" ADD CONSTRAINT "IDX_ROLE_TARGET_UNIQUE_USER_WORKSPACE" UNIQUE ("workspaceId", "userWorkspaceId")`,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run UpdateRoleTargetsUniqueConstraintMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
`Rollbacking UpdateRoleTargetsUniqueConstraintMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command';
|
||||
import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command';
|
||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
||||
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
||||
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-index.command';
|
||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.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 { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
IndexMetadataEntity,
|
||||
ViewEntity,
|
||||
ViewGroupEntity,
|
||||
FeatureFlagEntity,
|
||||
RoleTargetEntity,
|
||||
PageLayoutEntity,
|
||||
PageLayoutWidgetEntity,
|
||||
PageLayoutTabEntity,
|
||||
TimelineActivityWorkspaceEntity,
|
||||
]),
|
||||
DataSourceModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceCacheModule,
|
||||
FieldMetadataModule,
|
||||
ObjectMetadataModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
providers: [
|
||||
CleanEmptyStringNullInTextFieldsCommand,
|
||||
BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
DeduplicateRoleTargetsCommand,
|
||||
RenameIndexNameCommand,
|
||||
MigrateStandardInvalidEntitiesCommand,
|
||||
UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
MigrateTimelineActivityToMorphRelationsCommand,
|
||||
],
|
||||
exports: [
|
||||
CleanEmptyStringNullInTextFieldsCommand,
|
||||
BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
DeduplicateRoleTargetsCommand,
|
||||
RenameIndexNameCommand,
|
||||
MigrateStandardInvalidEntitiesCommand,
|
||||
UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
MigrateTimelineActivityToMorphRelationsCommand,
|
||||
],
|
||||
})
|
||||
export class V1_13_UpgradeVersionCommandModule {}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type';
|
||||
import { MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
const REMOVED_AGENT_STANDARD_IDS = [
|
||||
'20202020-0002-0001-0001-000000000006', // dashboard-builder
|
||||
'20202020-0002-0001-0001-000000000003', // data-manipulator
|
||||
'20202020-0002-0001-0001-000000000007', // metadata-builder
|
||||
'20202020-0002-0001-0001-000000000005', // researcher
|
||||
'20202020-0002-0001-0001-000000000001', // workflow-builder
|
||||
];
|
||||
|
||||
const REMOVED_ROLE_STANDARD_IDS = [
|
||||
'20202020-0001-0001-0001-000000000002', // workflow-manager
|
||||
'20202020-0001-0001-0001-000000000004', // data-manipulator
|
||||
'20202020-0001-0001-0001-000000000005', // dashboard-manager
|
||||
'20202020-0001-0001-0001-000000000006', // data-model-manager
|
||||
];
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-14:delete-removed-agents',
|
||||
description: 'Delete agents and roles that were removed from the codebase',
|
||||
})
|
||||
export class DeleteRemovedAgentsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly agentService: AgentService,
|
||||
private readonly roleService: RoleService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
const { flatAgentMaps, flatRoleMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatAgentMaps',
|
||||
'flatRoleMaps',
|
||||
]);
|
||||
|
||||
await this.deleteRemovedAgents({ flatAgentMaps, workspaceId, isDryRun });
|
||||
await this.deleteRemovedRoles({ flatRoleMaps, workspaceId, isDryRun });
|
||||
}
|
||||
|
||||
private async deleteRemovedAgents({
|
||||
flatAgentMaps,
|
||||
workspaceId,
|
||||
isDryRun,
|
||||
}: {
|
||||
flatAgentMaps: FlatAgentMaps;
|
||||
workspaceId: string;
|
||||
isDryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const allAgents = Object.values(flatAgentMaps.byId).filter(isDefined);
|
||||
|
||||
const agentsToDelete = allAgents.filter((agent) =>
|
||||
REMOVED_AGENT_STANDARD_IDS.includes(agent.standardId ?? ''),
|
||||
);
|
||||
|
||||
const agentNames = agentsToDelete.map((agent) => agent.name).join(', ');
|
||||
|
||||
this.logger.log(
|
||||
`DRY RUN: Would delete ${agentsToDelete.length} removed agent(s): ${agentNames}`,
|
||||
);
|
||||
|
||||
if (agentsToDelete.length === 0 || isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agentIds = agentsToDelete.map((agent) => agent.id);
|
||||
|
||||
try {
|
||||
await this.agentService.deleteManyAgents({
|
||||
ids: agentIds,
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to delete workspace agents \n ${JSON.stringify(error, null, 2)}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${agentsToDelete.length} removed agent(s): ${agentNames}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async deleteRemovedRoles({
|
||||
flatRoleMaps,
|
||||
workspaceId,
|
||||
isDryRun,
|
||||
}: {
|
||||
flatRoleMaps: MetadataFlatEntityMaps<'role'>;
|
||||
workspaceId: string;
|
||||
isDryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const allRoles = Object.values(flatRoleMaps.byId).filter(isDefined);
|
||||
|
||||
const rolesToDelete = allRoles.filter((role) =>
|
||||
REMOVED_ROLE_STANDARD_IDS.includes(role.standardId ?? ''),
|
||||
);
|
||||
|
||||
const roleLabels = rolesToDelete.map((role) => role.label).join(', ');
|
||||
|
||||
this.logger.log(
|
||||
`DRY RUN: Would delete ${rolesToDelete.length} removed role(s): ${roleLabels}`,
|
||||
);
|
||||
|
||||
if (rolesToDelete.length === 0 || isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleIds = rolesToDelete.map((role) => role.id);
|
||||
|
||||
try {
|
||||
await this.roleService.deleteManyRoles({
|
||||
ids: roleIds,
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to delete workspace roles \n ${JSON.stringify(error, null, 2)}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${rolesToDelete.length} removed role(s): ${roleLabels}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration/utils/compute-postgres-enum-name.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-14:udpate-created-by-enum',
|
||||
description: 'Add new APPLICATION value to createdBy enum',
|
||||
})
|
||||
export class UpdateCreatedByEnumCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(UpdateCreatedByEnumCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
protected readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
dataSource,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
if (!isDefined(dataSource)) {
|
||||
throw new Error(
|
||||
`Could not find data source for workspace ${workspaceId}, should never occur`,
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const createdByFields = await this.fieldMetadataRepository.find({
|
||||
where: { workspaceId, name: 'createdBy', type: FieldMetadataType.ACTOR },
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`Dry run mode: found ${createdByFields.length} createdBy enums to update`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
this.logger.log(`Updating ${createdByFields.length} createdBy enums`);
|
||||
try {
|
||||
for (const createdByField of createdByFields) {
|
||||
const tableName = computeTableName(
|
||||
createdByField.object.nameSingular,
|
||||
createdByField.object.isCustom,
|
||||
);
|
||||
const createdByEnumName = computePostgresEnumName({
|
||||
tableName,
|
||||
columnName: 'createdBySource',
|
||||
});
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "${schemaName}"."${createdByEnumName}" ADD VALUE IF NOT EXISTS 'APPLICATION'`,
|
||||
);
|
||||
}
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
`Rollbacking UpdateCreatedByEnumCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DeleteRemovedAgentsCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-delete-removed-agents.command';
|
||||
import { UpdateCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-update-created-by-enum.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity, FieldMetadataEntity]),
|
||||
DataSourceModule,
|
||||
AiAgentModule,
|
||||
RoleModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [UpdateCreatedByEnumCommand, DeleteRemovedAgentsCommand],
|
||||
exports: [DeleteRemovedAgentsCommand, UpdateCreatedByEnumCommand],
|
||||
})
|
||||
export class V1_14_UpgradeVersionCommandModule {}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { addWorkspaceForeignKeysQueries } from 'src/database/typeorm/core/migrations/utils/1767002571103-addWorkspaceForeignKeys.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-15:add-workspace-foreign-keys-migration',
|
||||
description: 'Add foreign key constraints for workspaceId on core tables',
|
||||
})
|
||||
export class AddWorkspaceForeignKeysMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.log(
|
||||
'Skipping has already been run once AddWorkspaceForeignKeysMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await addWorkspaceForeignKeysQueries(queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run AddWorkspaceForeignKeysMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
`Rollbacking AddWorkspaceForeignKeysMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import {
|
||||
ATTACHMENT_STANDARD_FIELD_IDS,
|
||||
COMPANY_STANDARD_FIELD_IDS,
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
DASHBOARD_STANDARD_FIELD_IDS,
|
||||
NOTE_STANDARD_FIELD_IDS,
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS,
|
||||
PERSON_STANDARD_FIELD_IDS,
|
||||
TASK_STANDARD_FIELD_IDS,
|
||||
WORKFLOW_RUN_STANDARD_FIELD_IDS,
|
||||
WORKFLOW_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-15:backfill-updated-by-field',
|
||||
description: 'Backfill updatedBy field for all objects',
|
||||
})
|
||||
export class BackfillUpdatedByFieldCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(BackfillUpdatedByFieldCommand.name);
|
||||
|
||||
private readonly objectToUpdatedByStandardIdMap: Record<string, string> = {
|
||||
[STANDARD_OBJECT_IDS.attachment]: ATTACHMENT_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.company]: COMPANY_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.person]: PERSON_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.note]: NOTE_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.task]: TASK_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.opportunity]: OPPORTUNITY_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.workflow]: WORKFLOW_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.workflowRun]:
|
||||
WORKFLOW_RUN_STANDARD_FIELD_IDS.updatedBy,
|
||||
[STANDARD_OBJECT_IDS.dashboard]: DASHBOARD_STANDARD_FIELD_IDS.updatedBy,
|
||||
};
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting backfill of updatedBy field for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const objectMetadataList = Object.values(
|
||||
flatObjectMetadataMaps.byId,
|
||||
).filter(isDefined);
|
||||
|
||||
if (objectMetadataList.length === 0) {
|
||||
this.logger.log(
|
||||
`No objects found that need updatedBy field for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const createFieldInputs: CreateFieldInput[] = [];
|
||||
|
||||
for (const flatObjectMetadata of objectMetadataList) {
|
||||
const isStandardObject = belongsToTwentyStandardApp(flatObjectMetadata);
|
||||
const updatedByStandardId = !isStandardObject
|
||||
? CUSTOM_OBJECT_STANDARD_FIELD_IDS.updatedBy
|
||||
: isDefined(flatObjectMetadata.standardId)
|
||||
? this.objectToUpdatedByStandardIdMap[flatObjectMetadata.standardId]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(updatedByStandardId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatFieldMetadatas = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatObjectMetadata.fieldIds,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const updatedByField = flatFieldMetadatas.find(
|
||||
(field) => field.standardId === updatedByStandardId,
|
||||
);
|
||||
|
||||
if (isDefined(updatedByField)) {
|
||||
this.logger.log(
|
||||
`Object ${flatObjectMetadata.nameSingular} (${flatObjectMetadata.standardId || 'custom'}) already has updatedBy field, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const applicationId = isStandardObject
|
||||
? twentyStandardFlatApplication.id
|
||||
: workspaceCustomFlatApplication.id;
|
||||
const universalIdentifier = isStandardObject ? updatedByStandardId : v4();
|
||||
|
||||
createFieldInputs.push({
|
||||
workspaceId,
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
label: 'Updated by',
|
||||
description: 'The user who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
standardId: updatedByStandardId,
|
||||
universalIdentifier,
|
||||
applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
if (createFieldInputs.length === 0) {
|
||||
this.logger.log(
|
||||
`All objects already have updatedBy field for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${createFieldInputs.length} objects that need updatedBy field`,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create updatedBy field for ${createFieldInputs.length} objects`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fieldMetadataService.createManyFields({
|
||||
createFieldInputs,
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create many fields \n ${JSON.stringify(error, null, 2)}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled updatedBy field for ${createFieldInputs.length} objects in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-15:fix-nan-position-values-in-notes',
|
||||
description: 'Fix NaN position values in notes by replacing them with 2',
|
||||
})
|
||||
export class FixNanPositionValuesInNotesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(
|
||||
FixNanPositionValuesInNotesCommand.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
dataSource,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun || false;
|
||||
|
||||
if (!isDefined(dataSource)) {
|
||||
throw new Error(
|
||||
`Could not find data source for workspace ${workspaceId}, should never occur`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log('Dry run mode: No changes will be applied');
|
||||
}
|
||||
|
||||
try {
|
||||
// Count NaN position values in notes
|
||||
const noteRepository = await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
'note',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const nanCount = await noteRepository.count({
|
||||
where: { position: 'NaN' },
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${nanCount} note(s) with NaN position values in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (nanCount === 0) {
|
||||
this.logger.log('No NaN position values to fix');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDryRun) {
|
||||
// Update NaN position values to 2 because 1 is first position
|
||||
await noteRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({ position: 2 })
|
||||
.where('position = :nanString', { nanString: 'NaN' })
|
||||
.execute();
|
||||
|
||||
this.logger.log(
|
||||
`Fixed ${nanCount} NaN position value(s) in notes for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would fix ${nanCount} NaN position value(s) in notes for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not fix NaN position values in notes for workspace ${workspaceId}`,
|
||||
);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
-299
@@ -1,299 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
enum DeprecatedGraphType {
|
||||
VERTICAL_BAR = 'VERTICAL_BAR',
|
||||
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
|
||||
PIE = 'PIE',
|
||||
LINE = 'LINE',
|
||||
AGGREGATE = 'AGGREGATE',
|
||||
GAUGE = 'GAUGE',
|
||||
}
|
||||
|
||||
const migrateConfiguration = (
|
||||
widgetType: WidgetType,
|
||||
configuration: Record<string, unknown>,
|
||||
): Record<string, unknown> => {
|
||||
const existingConfigurationType = configuration.configurationType as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (isDefined(existingConfigurationType)) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
switch (widgetType) {
|
||||
case WidgetType.GRAPH:
|
||||
return migrateGraphConfiguration(configuration);
|
||||
case WidgetType.IFRAME:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
};
|
||||
case WidgetType.STANDALONE_RICH_TEXT:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
|
||||
};
|
||||
case WidgetType.VIEW:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.VIEW,
|
||||
};
|
||||
case WidgetType.FIELDS:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
};
|
||||
case WidgetType.TIMELINE:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.TIMELINE,
|
||||
};
|
||||
case WidgetType.TASKS:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.TASKS,
|
||||
};
|
||||
case WidgetType.NOTES:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.NOTES,
|
||||
};
|
||||
case WidgetType.FILES:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.FILES,
|
||||
};
|
||||
case WidgetType.EMAILS:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.EMAILS,
|
||||
};
|
||||
case WidgetType.CALENDAR:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.CALENDAR,
|
||||
};
|
||||
case WidgetType.FIELD_RICH_TEXT:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.FIELD_RICH_TEXT,
|
||||
};
|
||||
case WidgetType.WORKFLOW:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.WORKFLOW,
|
||||
};
|
||||
case WidgetType.WORKFLOW_VERSION:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.WORKFLOW_VERSION,
|
||||
};
|
||||
case WidgetType.WORKFLOW_RUN:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.WORKFLOW_RUN,
|
||||
};
|
||||
case WidgetType.FIELD:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
};
|
||||
case WidgetType.FRONT_COMPONENT:
|
||||
return {
|
||||
...configuration,
|
||||
configurationType: WidgetConfigurationType.FRONT_COMPONENT,
|
||||
};
|
||||
default:
|
||||
assertUnreachable(widgetType);
|
||||
}
|
||||
};
|
||||
|
||||
const migrateGraphConfiguration = (
|
||||
configuration: Record<string, unknown>,
|
||||
): Record<string, unknown> => {
|
||||
const { graphType: _, ...restConfig } = configuration;
|
||||
const graphType = configuration.graphType as DeprecatedGraphType | undefined;
|
||||
|
||||
if (!isDefined(graphType)) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
switch (graphType) {
|
||||
case DeprecatedGraphType.VERTICAL_BAR:
|
||||
return {
|
||||
...restConfig,
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
};
|
||||
case DeprecatedGraphType.HORIZONTAL_BAR:
|
||||
return {
|
||||
...restConfig,
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
};
|
||||
case DeprecatedGraphType.PIE:
|
||||
return {
|
||||
...restConfig,
|
||||
configurationType: WidgetConfigurationType.PIE_CHART,
|
||||
};
|
||||
case DeprecatedGraphType.LINE:
|
||||
return {
|
||||
...restConfig,
|
||||
configurationType: WidgetConfigurationType.LINE_CHART,
|
||||
};
|
||||
case DeprecatedGraphType.AGGREGATE:
|
||||
return {
|
||||
...restConfig,
|
||||
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
|
||||
};
|
||||
case DeprecatedGraphType.GAUGE:
|
||||
return {
|
||||
...restConfig,
|
||||
configurationType: WidgetConfigurationType.GAUGE_CHART,
|
||||
};
|
||||
default:
|
||||
assertUnreachable(graphType);
|
||||
}
|
||||
};
|
||||
|
||||
const needsMigration = (
|
||||
widgetType: WidgetType,
|
||||
configuration: Record<string, unknown>,
|
||||
): boolean => {
|
||||
const graphType = configuration.graphType as DeprecatedGraphType | undefined;
|
||||
const configurationType = configuration.configurationType as
|
||||
| WidgetConfigurationType
|
||||
| undefined;
|
||||
|
||||
if (isDefined(graphType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(configurationType) &&
|
||||
(widgetType === WidgetType.IFRAME ||
|
||||
widgetType === WidgetType.STANDALONE_RICH_TEXT)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-15:migrate-page-layout-widget-configuration',
|
||||
description:
|
||||
'Migrate page layout widget configurations to use configurationType',
|
||||
})
|
||||
export class MigratePageLayoutWidgetConfigurationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(PageLayoutWidgetEntity)
|
||||
private readonly pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting migration of page layout widget configurations for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const widgets = await this.pageLayoutWidgetRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'type', 'configuration'],
|
||||
});
|
||||
|
||||
const widgetsWithNullConfiguration = widgets.filter(
|
||||
(widget) => !isDefined(widget.configuration),
|
||||
);
|
||||
|
||||
for (const widget of widgetsWithNullConfiguration) {
|
||||
this.logger.log(
|
||||
`Widget ${widget.id} has a corrupted configuration (null). Skipping migration.`,
|
||||
);
|
||||
}
|
||||
|
||||
const widgetsToMigrate = widgets.filter(
|
||||
(widget) =>
|
||||
isDefined(widget.configuration) &&
|
||||
needsMigration(
|
||||
widget.type,
|
||||
widget.configuration as unknown as Record<string, unknown>,
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${widgetsToMigrate.length} widget(s) needing migration out of ${widgets.length} total`,
|
||||
);
|
||||
|
||||
if (widgetsToMigrate.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const widget of widgetsToMigrate) {
|
||||
const migratedConfiguration = migrateConfiguration(
|
||||
widget.type,
|
||||
widget.configuration as unknown as Record<string, unknown>,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update widget ${widget.id} configuration from ${JSON.stringify(widget.configuration)} to ${JSON.stringify(migratedConfiguration)}`,
|
||||
);
|
||||
} else {
|
||||
await this.pageLayoutWidgetRepository.update(
|
||||
{ id: widget.id },
|
||||
{ configuration: migratedConfiguration },
|
||||
);
|
||||
|
||||
this.logger.log(`Updated widget ${widget.id} configuration`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${options.dryRun ? '[DRY RUN] Would have migrated' : 'Successfully migrated'} ${widgetsToMigrate.length} widget(s)`,
|
||||
);
|
||||
|
||||
if (!options.dryRun && widgetsToMigrate.length > 0) {
|
||||
this.logger.log(
|
||||
`Invalidating and recomputing cache for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const flatCacheToInvalidate =
|
||||
getMetadataRelatedMetadataNames('pageLayoutWidget');
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
...flatCacheToInvalidate.map(getMetadataFlatEntityMapsKey),
|
||||
]);
|
||||
|
||||
this.logger.log(`Cache invalidated and recomputed successfully`);
|
||||
}
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddWorkspaceForeignKeysMigrationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-add-workspace-foreign-keys-migration.command';
|
||||
import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-backfill-updated-by-field.command';
|
||||
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
|
||||
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
PageLayoutWidgetEntity,
|
||||
ObjectMetadataEntity,
|
||||
ViewEntity,
|
||||
ViewFieldEntity,
|
||||
]),
|
||||
DataSourceModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
WorkspaceCacheModule,
|
||||
FieldMetadataModule,
|
||||
ViewFieldModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
providers: [
|
||||
MigratePageLayoutWidgetConfigurationCommand,
|
||||
FixNanPositionValuesInNotesCommand,
|
||||
BackfillUpdatedByFieldCommand,
|
||||
AddWorkspaceForeignKeysMigrationCommand,
|
||||
],
|
||||
exports: [
|
||||
MigratePageLayoutWidgetConfigurationCommand,
|
||||
FixNanPositionValuesInNotesCommand,
|
||||
BackfillUpdatedByFieldCommand,
|
||||
AddWorkspaceForeignKeysMigrationCommand,
|
||||
],
|
||||
})
|
||||
export class V1_15_UpgradeVersionCommandModule {}
|
||||
-321
@@ -1,321 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:backfill-opportunity-owner-field',
|
||||
description: 'Backfill owner field for opportunity object',
|
||||
})
|
||||
export class BackfillOpportunityOwnerFieldCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(
|
||||
BackfillOpportunityOwnerFieldCommand.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting backfill of owner field for opportunity in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const opportunityObjectMetadata = Object.values(
|
||||
flatObjectMetadataMaps.byId,
|
||||
).find(
|
||||
(obj) =>
|
||||
isDefined(obj) && obj.standardId === STANDARD_OBJECT_IDS.opportunity,
|
||||
);
|
||||
|
||||
if (!isDefined(opportunityObjectMetadata)) {
|
||||
this.logger.log(
|
||||
`Opportunity object not found for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceMemberObjectMetadata = Object.values(
|
||||
flatObjectMetadataMaps.byId,
|
||||
).find(
|
||||
(obj) =>
|
||||
isDefined(obj) &&
|
||||
obj.standardId === STANDARD_OBJECT_IDS.workspaceMember,
|
||||
);
|
||||
|
||||
if (!isDefined(workspaceMemberObjectMetadata)) {
|
||||
this.logger.log(
|
||||
`WorkspaceMember object not found for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const flatFieldMetadatas = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: opportunityObjectMetadata.fieldIds,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const ownerUniversalIdentifier =
|
||||
STANDARD_OBJECTS.opportunity.fields.owner.universalIdentifier;
|
||||
|
||||
const ownerField = flatFieldMetadatas.find(
|
||||
(field) => field.universalIdentifier === ownerUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (isDefined(ownerField)) {
|
||||
this.logger.log(
|
||||
`Opportunity already has owner field in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const customOwnerField =
|
||||
flatFieldMetadatas.find(
|
||||
(field) =>
|
||||
field.name === 'owner' &&
|
||||
field.universalIdentifier !== ownerUniversalIdentifier,
|
||||
// Some self hoster might have run the name renaming successfully but not the joinColumnName
|
||||
// Rather than asking them to restore db also grabbing them here
|
||||
) ?? flatFieldMetadatas.find((field) => field.name === 'ownerOld');
|
||||
|
||||
if (isDefined(customOwnerField)) {
|
||||
await this.renameCustomOwnerFieldToOwnerOld({
|
||||
customOwnerField,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceId,
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create owner field for opportunity in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const createFieldInput: CreateFieldInput = {
|
||||
workspaceId,
|
||||
objectMetadataId: opportunityObjectMetadata.id,
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Owner',
|
||||
description: 'Opportunity owner',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
standardId: ownerUniversalIdentifier,
|
||||
universalIdentifier: ownerUniversalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId: workspaceMemberObjectMetadata.id,
|
||||
targetFieldLabel: 'Owned opportunities',
|
||||
targetFieldIcon: 'IconTargetArrow',
|
||||
},
|
||||
};
|
||||
|
||||
const ownedOpportunitiesUniversalIdentifier =
|
||||
STANDARD_OBJECTS.workspaceMember.fields.ownedOpportunities
|
||||
.universalIdentifier;
|
||||
|
||||
try {
|
||||
await this.fieldMetadataService.createManyFields({
|
||||
createFieldInputs: [createFieldInput],
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Created owner field for opportunity in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const ownedOpportunitiesField =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
objectMetadataId: workspaceMemberObjectMetadata.id,
|
||||
name: 'ownedOpportunities',
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(ownedOpportunitiesField)) {
|
||||
await this.fieldMetadataRepository.update(
|
||||
{ id: ownedOpportunitiesField.id },
|
||||
{
|
||||
universalIdentifier: ownedOpportunitiesUniversalIdentifier,
|
||||
standardId: ownedOpportunitiesUniversalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Updated ownedOpportunities field universalIdentifier in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Should never occur could not find the created ownedOpportunities field`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled owner field for opportunity in workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create owner field for opportunity in workspace ${workspaceId}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async renameCustomOwnerFieldToOwnerOld({
|
||||
customOwnerField,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceId,
|
||||
dryRun,
|
||||
}: {
|
||||
customOwnerField: FlatFieldMetadata;
|
||||
flatFieldMetadataMaps: MetadataFlatEntityMaps<'fieldMetadata'>;
|
||||
workspaceId: string;
|
||||
dryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would rename custom owner field to 'ownerOld' in workspace and search for colliding foreignKey ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isMorphOrRelationField =
|
||||
isMorphOrRelationFlatFieldMetadata(customOwnerField);
|
||||
const hasCollidingJoinColumnName =
|
||||
isMorphOrRelationField &&
|
||||
isDefined(customOwnerField.settings.joinColumnName) &&
|
||||
customOwnerField.settings.joinColumnName === 'ownerId';
|
||||
|
||||
await this.fieldMetadataService.updateOneField({
|
||||
updateFieldInput: {
|
||||
id: customOwnerField.id,
|
||||
name: 'ownerOld',
|
||||
label: 'Owner (Old)',
|
||||
...(hasCollidingJoinColumnName
|
||||
? {
|
||||
settings: {
|
||||
...customOwnerField.settings,
|
||||
joinColumnName: `ownerOldId`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
this.logger.log(
|
||||
`Renamed custom owner field to 'ownerOld' in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!isMorphOrRelationField || hasCollidingJoinColumnName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: customOwnerField.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(relatedFlatFieldMetadata) ||
|
||||
!isMorphOrRelationFlatFieldMetadata(relatedFlatFieldMetadata)
|
||||
) {
|
||||
this.logger.error(
|
||||
`Could not find custom owner relation target field ${customOwnerField.relationTargetFieldMetadataId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const targetHasCollidingJoinColumnName =
|
||||
isDefined(relatedFlatFieldMetadata.settings.joinColumnName) &&
|
||||
relatedFlatFieldMetadata.settings.joinColumnName === 'ownerId';
|
||||
|
||||
if (!targetHasCollidingJoinColumnName) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fieldMetadataService.updateOneField({
|
||||
updateFieldInput: {
|
||||
id: relatedFlatFieldMetadata.id,
|
||||
settings: {
|
||||
...relatedFlatFieldMetadata.settings,
|
||||
joinColumnName: `ownerOldId`,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Renamed custom owner field target related field join column name to 'ownerOldId' in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { updateStandardPageLayoutWidgetsWithWorkspaceFieldIds } from 'src/database/commands/upgrade-version-command/1-16/utils/update-standard-page-layout-widgets-with-workspace-field-ids.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { MY_FIRST_DASHBOARD_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-dashboards';
|
||||
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { DashboardWorkspaceEntity } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:backfill-standard-page-layouts',
|
||||
description: 'Backfill standard page layouts and dashboard records',
|
||||
})
|
||||
export class BackfillStandardPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(
|
||||
BackfillStandardPageLayoutsCommand.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting backfill of standard page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.backfillStandardPageLayoutMetadata({ workspaceId, options });
|
||||
await this.backfillDashboardRecord({ workspaceId, options });
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled standard page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillStandardPageLayoutMetadata({
|
||||
workspaceId,
|
||||
options,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
options: RunOnWorkspaceArgs['options'];
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatPageLayoutMaps: fromFlatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps: fromFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: fromFlatPageLayoutWidgetMaps,
|
||||
featureFlagsMap,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'featureFlagsMap',
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const flatPageLayout = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: fromFlatPageLayoutMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.universalIdentifier,
|
||||
});
|
||||
|
||||
if (isDefined(flatPageLayout)) {
|
||||
this.logger.log(
|
||||
`Standard page layout already exists in workspace ${workspaceId}, skipping metadata backfill`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const shouldIncludeRecordPageLayouts = this.twentyConfigService.get(
|
||||
'SHOULD_SEED_STANDARD_RECORD_PAGE_LAYOUTS',
|
||||
);
|
||||
|
||||
const {
|
||||
flatPageLayoutMaps: toFlatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps: toFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: rawToFlatPageLayoutWidgetMaps,
|
||||
} = computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
shouldIncludeRecordPageLayouts,
|
||||
});
|
||||
|
||||
const toFlatPageLayoutWidgetMaps =
|
||||
updateStandardPageLayoutWidgetsWithWorkspaceFieldIds({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: rawToFlatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create standard page layout entities ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndbuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo(
|
||||
{
|
||||
additionalCacheDataMaps: {
|
||||
featureFlagsMap,
|
||||
},
|
||||
buildOptions: {
|
||||
isSystemBuild: true,
|
||||
},
|
||||
fromToAllFlatEntityMaps: {
|
||||
flatPageLayoutMaps: {
|
||||
from: fromFlatPageLayoutMaps,
|
||||
to: toFlatPageLayoutMaps,
|
||||
},
|
||||
flatPageLayoutTabMaps: {
|
||||
from: fromFlatPageLayoutTabMaps,
|
||||
to: toFlatPageLayoutTabMaps,
|
||||
},
|
||||
flatPageLayoutWidgetMaps: {
|
||||
from: fromFlatPageLayoutWidgetMaps,
|
||||
to: toFlatPageLayoutWidgetMaps,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
dependencyAllFlatEntityMaps: {
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndbuildResult)) {
|
||||
this.logger.error(
|
||||
`Failed to create many fields \n ${JSON.stringify(validateAndbuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard page layout/tab/widgets on workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled standard page layout metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillDashboardRecord({
|
||||
workspaceId,
|
||||
options,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
options: RunOnWorkspaceArgs['options'];
|
||||
}): Promise<void> {
|
||||
const dashboardRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<DashboardWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'dashboard',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const existingDashboard = await dashboardRepository.findOne({
|
||||
where: { id: MY_FIRST_DASHBOARD_ID },
|
||||
});
|
||||
|
||||
if (isDefined(existingDashboard)) {
|
||||
this.logger.log(
|
||||
`Dashboard already exists for workspace ${workspaceId}, skipping dashboard backfill`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create dashboard record for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatPageLayoutMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
]);
|
||||
|
||||
const myFirstDashboardPageLayout = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatPageLayoutMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(myFirstDashboardPageLayout)) {
|
||||
throw new Error(
|
||||
`Page layout with universalIdentifier '${STANDARD_PAGE_LAYOUTS.myFirstDashboard.universalIdentifier}' not found`,
|
||||
);
|
||||
}
|
||||
|
||||
await dashboardRepository.insert({
|
||||
id: MY_FIRST_DASHBOARD_ID,
|
||||
title: 'My First Dashboard',
|
||||
pageLayoutId: myFirstDashboardPageLayout.id,
|
||||
position: 0,
|
||||
createdBy: {
|
||||
source: FieldActorSource.SYSTEM,
|
||||
workspaceMemberId: null,
|
||||
name: 'System',
|
||||
context: {},
|
||||
},
|
||||
updatedBy: {
|
||||
source: FieldActorSource.SYSTEM,
|
||||
workspaceMemberId: null,
|
||||
name: 'System',
|
||||
context: {},
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled dashboard record for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:flush-v2-cache-and-increment-metadata-version',
|
||||
description: 'Flush the whole v2 cache and increment the metadata version',
|
||||
})
|
||||
export class FlushV2CacheAndIncrementMetadataVersionCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Flushing v2 cache and incrementing metadata version for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would flush v2 cache and increment metadata version for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRunnerService.invalidateCache({
|
||||
allFlatEntityMapsKeys: ALL_FLAT_ENTITY_MAPS_PROPERTIES,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully flushed v2 cache and incremented metadata version for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_AGENT } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-agent.constant';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-agent-metadata',
|
||||
description: 'Identify standard agent metadata',
|
||||
})
|
||||
export class IdentifyAgentMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard agent metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
await this.identifyStandardAgent({
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomAgents({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('agent');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: flatAgentMaps ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatAgentMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardAgent({
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const helperAgent = await this.agentRepository.findOne({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
name: 'helper',
|
||||
isCustom: false,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (!isDefined(helperAgent)) {
|
||||
this.logger.warn(
|
||||
`Standard agent "helper" not found for workspace ${workspaceId}, skipping standard agent identification`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(helperAgent.applicationId)) {
|
||||
this.logger.warn(
|
||||
`Standard agent "helper" already has applicationId set, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
` - Standard agent "helper" (id=${helperAgent.id}) -> universalIdentifier=${STANDARD_AGENT.helper.universalIdentifier}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.agentRepository.save({
|
||||
id: helperAgent.id,
|
||||
universalIdentifier: STANDARD_AGENT.helper.universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomAgents({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomAgents = await this.agentRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomAgents.map((agentEntity) => ({
|
||||
id: agentEntity.id,
|
||||
universalIdentifier: agentEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom agent(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.agentRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
RunOnWorkspaceArgs,
|
||||
WorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
type CustomFieldMetadata = {
|
||||
fieldMetadataEntity: FieldMetadataEntity;
|
||||
fromStandard: boolean;
|
||||
};
|
||||
|
||||
type StandardFieldMetadata = {
|
||||
fieldMetadataEntity: FieldMetadataEntity;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
type AllWarnings = 'unknown_standard_id';
|
||||
|
||||
type FieldMetadataWarning = {
|
||||
fieldMetadataEntity: FieldMetadataEntity;
|
||||
warning: AllWarnings;
|
||||
};
|
||||
|
||||
type AllExceptions = 'existing_universal_id_mismatch';
|
||||
|
||||
type FieldMetadataException = {
|
||||
fieldMetadataEntity: FieldMetadataEntity;
|
||||
exception: AllExceptions;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-field-metadata',
|
||||
description: 'Identify standard field metadata',
|
||||
})
|
||||
export class IdentifyFieldMetadataCommand extends WorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
WorkspaceActivationStatus.ONGOING_CREATION,
|
||||
WorkspaceActivationStatus.PENDING_CREATION,
|
||||
]);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const allFieldMetadataEntities = await this.fieldMetadataRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
name: true,
|
||||
standardId: true,
|
||||
object: {
|
||||
nameSingular: true,
|
||||
},
|
||||
isCustom: true,
|
||||
},
|
||||
relations: ['object'],
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const customFieldMetadataEntities: CustomFieldMetadata[] = [];
|
||||
const standardFieldMetadataEntities: StandardFieldMetadata[] = [];
|
||||
const warnings: FieldMetadataWarning[] = [];
|
||||
const exceptions: FieldMetadataException[] = [];
|
||||
|
||||
for (const fieldMetadataEntity of allFieldMetadataEntities) {
|
||||
const shouldBelongToTwentyStandardApp =
|
||||
!fieldMetadataEntity.isCustom &&
|
||||
isDefined(fieldMetadataEntity.standardId);
|
||||
|
||||
if (!shouldBelongToTwentyStandardApp) {
|
||||
customFieldMetadataEntities.push({
|
||||
fieldMetadataEntity,
|
||||
fromStandard: false,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectConfig =
|
||||
STANDARD_OBJECTS[
|
||||
fieldMetadataEntity.object
|
||||
.nameSingular as keyof typeof STANDARD_OBJECTS
|
||||
];
|
||||
const universalIdentifier =
|
||||
objectConfig?.fields[
|
||||
fieldMetadataEntity.name as keyof typeof objectConfig.fields
|
||||
]?.universalIdentifier;
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
warnings.push({
|
||||
fieldMetadataEntity,
|
||||
warning: 'unknown_standard_id',
|
||||
});
|
||||
customFieldMetadataEntities.push({
|
||||
fieldMetadataEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(fieldMetadataEntity.universalIdentifier) &&
|
||||
fieldMetadataEntity.universalIdentifier !== universalIdentifier
|
||||
) {
|
||||
exceptions.push({
|
||||
fieldMetadataEntity,
|
||||
exception: 'existing_universal_id_mismatch',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
standardFieldMetadataEntities.push({
|
||||
fieldMetadataEntity,
|
||||
universalIdentifier:
|
||||
fieldMetadataEntity.universalIdentifier ?? universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
const totalUpdates =
|
||||
customFieldMetadataEntities.length + standardFieldMetadataEntities.length;
|
||||
|
||||
if (warnings.length > 0) {
|
||||
this.logger.warn(
|
||||
`Found ${warnings.length} warning(s) while processing field metadata for workspace ${workspaceId}. These fields will become custom.`,
|
||||
);
|
||||
|
||||
for (const { fieldMetadataEntity, warning } of warnings) {
|
||||
this.logger.warn(
|
||||
`Warning for field "${fieldMetadataEntity.name}" on object "${fieldMetadataEntity.object.nameSingular}" (id=${fieldMetadataEntity.id} standardId=${fieldMetadataEntity.standardId}): ${warning}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions.length > 0) {
|
||||
this.logger.error(
|
||||
`Found ${exceptions.length} exception(s) while processing field metadata for workspace ${workspaceId}. No updates will be applied.`,
|
||||
);
|
||||
|
||||
for (const { fieldMetadataEntity, exception } of exceptions) {
|
||||
this.logger.error(
|
||||
`Exception for field "${fieldMetadataEntity.name}" on object "${fieldMetadataEntity.object.nameSingular}" (id=${fieldMetadataEntity.id} standardId=${fieldMetadataEntity.standardId}): ${exception}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully validated ${totalUpdates}/${allFieldMetadataEntities.length} field metadata update(s) for workspace ${workspaceId} (${customFieldMetadataEntities.length} custom, ${standardFieldMetadataEntities.length} standard)`,
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
const customUpdates = customFieldMetadataEntities.map(
|
||||
({ fieldMetadataEntity }) => ({
|
||||
id: fieldMetadataEntity.id,
|
||||
universalIdentifier: fieldMetadataEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const standardUpdates = standardFieldMetadataEntities.map(
|
||||
({ fieldMetadataEntity, universalIdentifier }) => ({
|
||||
id: fieldMetadataEntity.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.fieldMetadataRepository.save([
|
||||
...customUpdates,
|
||||
...standardUpdates,
|
||||
]);
|
||||
|
||||
const relatedMetadataNames =
|
||||
getMetadataRelatedMetadataNames('fieldMetadata');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: flatFieldMetadataMaps ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
|
||||
this.logger.log(
|
||||
`Applied ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Dry run: would apply ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-341
@@ -1,341 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
import { STANDARD_INDEX_FIELD_UNIVERSAL_IDENTIFIERS } from './constants/standard-index-field-names.constant';
|
||||
|
||||
type StandardIndexUpdate = {
|
||||
flatIndexMetadata: FlatIndexMetadata;
|
||||
universalIdentifier: string;
|
||||
objectNameSingular: string;
|
||||
indexName: string;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-index-metadata',
|
||||
description: 'Identify standard index metadata',
|
||||
})
|
||||
export class IdentifyIndexMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(IndexMetadataEntity)
|
||||
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard index metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await this.identifyStandardIndexesOrThrow({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomIndexes({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('index');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatIndexMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardIndexesOrThrow({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const standardIndexUpdates: StandardIndexUpdate[] = [];
|
||||
|
||||
for (const [objectNameSingular, objectConfig] of Object.entries(
|
||||
STANDARD_OBJECTS,
|
||||
)) {
|
||||
const objectIndexes =
|
||||
'indexes' in objectConfig
|
||||
? (objectConfig.indexes as Record<
|
||||
string,
|
||||
{ universalIdentifier: string } | undefined
|
||||
>)
|
||||
: null;
|
||||
|
||||
if (
|
||||
!isDefined(objectIndexes) ||
|
||||
Object.keys(objectIndexes).length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: objectConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
this.logger.error(
|
||||
`Standard object "${objectNameSingular}" not found in workspace, this needs investigation, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectFlatIndexMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatObjectMetadata.indexMetadataIds,
|
||||
flatEntityMaps: flatIndexMaps,
|
||||
});
|
||||
|
||||
for (const [indexName, indexConfig] of Object.entries(objectIndexes)) {
|
||||
if (!isDefined(indexConfig)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const indexUniversalIdentifier = indexConfig.universalIdentifier;
|
||||
|
||||
if (!isDefined(indexUniversalIdentifier)) {
|
||||
this.logger.warn(
|
||||
`Index "${indexName}" config not found for object "${objectNameSingular}", skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedFieldUniversalIdentifiers =
|
||||
STANDARD_INDEX_FIELD_UNIVERSAL_IDENTIFIERS[objectNameSingular]?.[
|
||||
indexName
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(relatedFieldUniversalIdentifiers) ||
|
||||
relatedFieldUniversalIdentifiers.length === 0
|
||||
) {
|
||||
this.logger.warn(
|
||||
`No field mapping found for index "${indexName}" on object "${objectNameSingular}", skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const expectedFlatFieldMetadatas = relatedFieldUniversalIdentifiers
|
||||
.map((fieldUniversalIdentifier) =>
|
||||
findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier: fieldUniversalIdentifier,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (
|
||||
expectedFlatFieldMetadatas.length !==
|
||||
relatedFieldUniversalIdentifiers.length
|
||||
) {
|
||||
this.logger.warn(
|
||||
`Could not resolve all field metadata for index "${indexName}" on object "${objectNameSingular}", skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const expectedFieldMetadataIds = new Set(
|
||||
expectedFlatFieldMetadatas.map((flatField) => flatField.id),
|
||||
);
|
||||
const matchingFlatIndexMetadata = objectFlatIndexMetadatas.find(
|
||||
(flatIndex) => {
|
||||
const indexFieldMetadataIds = new Set(
|
||||
flatIndex.flatIndexFieldMetadatas.map(
|
||||
(indexField) => indexField.fieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
return this.setsEqual(
|
||||
indexFieldMetadataIds,
|
||||
expectedFieldMetadataIds,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(matchingFlatIndexMetadata)) {
|
||||
this.logger.warn(
|
||||
`Could not find matching index for "${indexName}" on object "${objectNameSingular}", skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDefined(matchingFlatIndexMetadata.applicationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
standardIndexUpdates.push({
|
||||
flatIndexMetadata: matchingFlatIndexMetadata,
|
||||
universalIdentifier: indexUniversalIdentifier,
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
indexName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const standardUpdates = standardIndexUpdates.map(
|
||||
({ flatIndexMetadata, universalIdentifier }) => ({
|
||||
id: flatIndexMetadata.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${standardUpdates.length} standard index(es) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
flatIndexMetadata,
|
||||
universalIdentifier,
|
||||
objectNameSingular,
|
||||
indexName,
|
||||
} of standardIndexUpdates) {
|
||||
this.logger.log(
|
||||
` - Standard index "${indexName}" on object "${objectNameSingular}" (id=${flatIndexMetadata.id}) -> universalIdentifier=${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.indexMetadataRepository.save(standardUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomIndexes({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomIndexes = await this.indexMetadataRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
name: true,
|
||||
objectMetadataId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomIndexes.map((indexEntity) => ({
|
||||
id: indexEntity.id,
|
||||
universalIdentifier: indexEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom index(es) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const indexEntity of remainingCustomIndexes) {
|
||||
const flatObjectMetadata =
|
||||
flatObjectMetadataMaps.byId[indexEntity.objectMetadataId];
|
||||
|
||||
this.logger.log(
|
||||
` - Custom index "${indexEntity.name}" on object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${indexEntity.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.indexMetadataRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private setsEqual(a: Set<string>, b: Set<string>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const value of a) {
|
||||
if (!b.has(value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
-233
@@ -1,233 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
RunOnWorkspaceArgs,
|
||||
WorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
type CustomObjectMetadata = {
|
||||
objectMetadataEntity: ObjectMetadataEntity;
|
||||
fromStandard: boolean;
|
||||
};
|
||||
|
||||
type StandardObjectMetadata = {
|
||||
objectMetadataEntity: ObjectMetadataEntity;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
type AllWarnings = 'unknown_standard_id';
|
||||
|
||||
type ObjectMetadataWarning = {
|
||||
objectMetadataEntity: ObjectMetadataEntity;
|
||||
warning: AllWarnings;
|
||||
};
|
||||
|
||||
type AllExceptions = 'existing_universal_id_mismatch';
|
||||
|
||||
type ObjectMetadataException = {
|
||||
objectMetadataEntity: ObjectMetadataEntity;
|
||||
exception: AllExceptions;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-object-metadata',
|
||||
description: 'Identify standard object metadata',
|
||||
})
|
||||
export class IdentifyObjectMetadataCommand extends WorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
WorkspaceActivationStatus.ONGOING_CREATION,
|
||||
]);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard object metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const allObjectMetadataEntities = await this.objectMetadataRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
nameSingular: true,
|
||||
standardId: true,
|
||||
isCustom: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const customObjectMetadataEntities: CustomObjectMetadata[] = [];
|
||||
const standardObjectMetadataEntities: StandardObjectMetadata[] = [];
|
||||
const warnings: ObjectMetadataWarning[] = [];
|
||||
const exceptions: ObjectMetadataException[] = [];
|
||||
|
||||
for (const objectMetadataEntity of allObjectMetadataEntities) {
|
||||
const shouldBelongToTwentyStandardApp =
|
||||
!objectMetadataEntity.isCustom &&
|
||||
isDefined(objectMetadataEntity.standardId);
|
||||
|
||||
if (!shouldBelongToTwentyStandardApp) {
|
||||
customObjectMetadataEntities.push({
|
||||
objectMetadataEntity,
|
||||
fromStandard: false,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectConfig =
|
||||
STANDARD_OBJECTS[
|
||||
objectMetadataEntity.nameSingular as keyof typeof STANDARD_OBJECTS
|
||||
];
|
||||
const universalIdentifier = objectConfig?.universalIdentifier;
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
warnings.push({
|
||||
objectMetadataEntity,
|
||||
warning: 'unknown_standard_id',
|
||||
});
|
||||
customObjectMetadataEntities.push({
|
||||
objectMetadataEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(objectMetadataEntity.universalIdentifier) &&
|
||||
objectMetadataEntity.universalIdentifier !== universalIdentifier
|
||||
) {
|
||||
exceptions.push({
|
||||
objectMetadataEntity,
|
||||
exception: 'existing_universal_id_mismatch',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
standardObjectMetadataEntities.push({
|
||||
objectMetadataEntity,
|
||||
universalIdentifier:
|
||||
objectMetadataEntity.universalIdentifier ?? universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
const totalUpdates =
|
||||
customObjectMetadataEntities.length +
|
||||
standardObjectMetadataEntities.length;
|
||||
|
||||
if (warnings.length > 0) {
|
||||
this.logger.warn(
|
||||
`Found ${warnings.length} warning(s) while processing object metadata for workspace ${workspaceId}. These objects will become custom.`,
|
||||
);
|
||||
|
||||
for (const { objectMetadataEntity, warning } of warnings) {
|
||||
this.logger.warn(
|
||||
`Warning for object "${objectMetadataEntity.nameSingular}" (id=${objectMetadataEntity.id} standardId=${objectMetadataEntity.standardId}): ${warning}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions.length > 0) {
|
||||
this.logger.error(
|
||||
`Found ${exceptions.length} exception(s) while processing object metadata for workspace ${workspaceId}. No updates will be applied.`,
|
||||
);
|
||||
|
||||
for (const { objectMetadataEntity, exception } of exceptions) {
|
||||
this.logger.error(
|
||||
`Exception for object "${objectMetadataEntity.nameSingular}" (id=${objectMetadataEntity.id} standardId=${objectMetadataEntity.standardId}): ${exception}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully validated ${totalUpdates}/${allObjectMetadataEntities.length} object metadata update(s) for workspace ${workspaceId} (${customObjectMetadataEntities.length} custom, ${standardObjectMetadataEntities.length} standard)`,
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
const customUpdates = customObjectMetadataEntities.map(
|
||||
({ objectMetadataEntity }) => ({
|
||||
id: objectMetadataEntity.id,
|
||||
universalIdentifier: objectMetadataEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const standardUpdates = standardObjectMetadataEntities.map(
|
||||
({ objectMetadataEntity, universalIdentifier }) => ({
|
||||
id: objectMetadataEntity.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.objectMetadataRepository.save([
|
||||
...customUpdates,
|
||||
...standardUpdates,
|
||||
]);
|
||||
|
||||
const relatedMetadataNames =
|
||||
getMetadataRelatedMetadataNames('objectMetadata');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: flatObjectMetadataMaps ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
|
||||
this.logger.log(
|
||||
`Applied ${totalUpdates} object metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Dry run: would apply ${totalUpdates} object metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { DataSource, IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ALL_METADATA_ENTITY_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
const REMAINING_ENTITIES_METADATA_NAMES = [
|
||||
'roleTarget',
|
||||
'rowLevelPermissionPredicate',
|
||||
'rowLevelPermissionPredicateGroup',
|
||||
'viewFilterGroup',
|
||||
'logicFunction',
|
||||
'skill',
|
||||
'pageLayoutWidget',
|
||||
'pageLayout',
|
||||
'pageLayoutTab',
|
||||
] as const satisfies AllMetadataName[];
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-remaining-entities-metadata',
|
||||
description:
|
||||
'Identify remaining entities metadata (roleTarget, rowLevelPermissionPredicate, rowLevelPermissionPredicateGroup, viewFilterGroup, viewSort, logicFunction, skill, pageLayoutWidget, pageLayout, pageLayoutTab)',
|
||||
})
|
||||
export class IdentifyRemainingEntitiesMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify remaining entities metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
for (const metadataName of REMAINING_ENTITIES_METADATA_NAMES) {
|
||||
await this.identifyEntitiesForMetadataName({
|
||||
metadataName,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
await this.identifyViewSortEntities({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
private async identifyEntitiesForMetadataName({
|
||||
metadataName,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
metadataName: AllMetadataName;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const entityClass = ALL_METADATA_ENTITY_BY_METADATA_NAME[metadataName];
|
||||
const repository = this.coreDataSource.getRepository(entityClass);
|
||||
|
||||
const entitiesWithoutApplicationId = await repository.find({
|
||||
select: ['id', 'universalIdentifier', 'applicationId'],
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (entitiesWithoutApplicationId.length === 0) {
|
||||
this.logger.log(
|
||||
`No ${metadataName} entities found without applicationId for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = entitiesWithoutApplicationId.map(
|
||||
(entity: SyncableEntity & { id: string }) => ({
|
||||
id: entity.id,
|
||||
universalIdentifier: entity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${updates.length} ${metadataName} entities to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await repository.save(updates);
|
||||
}
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames(metadataName);
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${flatEntityMapsKey} ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
flatEntityMapsKey,
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyViewSortEntities({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const viewSortRepository =
|
||||
this.coreDataSource.getRepository(ViewSortEntity);
|
||||
|
||||
const viewSortsWithoutApplicationId = await viewSortRepository.find({
|
||||
select: ['id', 'universalIdentifier', 'applicationId'],
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (viewSortsWithoutApplicationId.length === 0) {
|
||||
this.logger.log(
|
||||
`No viewSort entities found without applicationId for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = viewSortsWithoutApplicationId.map((viewSort) => ({
|
||||
id: viewSort.id,
|
||||
universalIdentifier: viewSort.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${updates.length} viewSort entities to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await viewSortRepository.save(updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-role-metadata',
|
||||
description: 'Identify standard role metadata',
|
||||
})
|
||||
export class IdentifyRoleMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard role metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
await this.identifyStandardRole({
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomRoles({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('role');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: flatRoleMaps ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatRoleMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardRole({
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const adminRole = await this.roleRepository.findOne({
|
||||
select: {
|
||||
id: true,
|
||||
label: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
label: 'Admin',
|
||||
isEditable: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(adminRole)) {
|
||||
this.logger.warn(
|
||||
`Standard role "Admin" not found for workspace ${workspaceId}, skipping standard role identification`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(adminRole.applicationId)) {
|
||||
this.logger.warn(
|
||||
`Standard role "Admin" already has applicationId set, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
` - Standard role "Admin" (id=${adminRole.id}) -> universalIdentifier=${STANDARD_ROLE.admin.universalIdentifier}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.roleRepository.save({
|
||||
id: adminRole.id,
|
||||
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomRoles({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomRoles = await this.roleRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomRoles.map((roleEntity) => ({
|
||||
id: roleEntity.id,
|
||||
universalIdentifier: roleEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom role(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.roleRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-383
@@ -1,383 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
type StandardViewFieldUpdate = {
|
||||
flatViewField: FlatViewField;
|
||||
universalIdentifier: string;
|
||||
objectNameSingular: string;
|
||||
viewName: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
type AllExceptions = 'unknown_standard_view_field';
|
||||
|
||||
type ViewFieldMetadataException = {
|
||||
flatViewField: FlatViewField;
|
||||
exception: AllExceptions;
|
||||
objectNameSingular: string;
|
||||
viewName: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-view-field-metadata',
|
||||
description: 'Identify standard view field metadata',
|
||||
})
|
||||
export class IdentifyViewFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard view field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await this.identifyStandardViewFieldsOrThrow({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomViewFields({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('viewField');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatViewFieldMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardViewFieldsOrThrow({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
flatViewFieldMaps: FlatEntityMaps<FlatViewField>;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const standardViewFieldUpdates: StandardViewFieldUpdate[] = [];
|
||||
const exceptions: ViewFieldMetadataException[] = [];
|
||||
|
||||
for (const [objectNameSingular, objectConfig] of Object.entries(
|
||||
STANDARD_OBJECTS,
|
||||
)) {
|
||||
const objectViews =
|
||||
'views' in objectConfig
|
||||
? (objectConfig.views as Record<
|
||||
string,
|
||||
| {
|
||||
universalIdentifier: string;
|
||||
viewFields?: Record<
|
||||
string,
|
||||
{ universalIdentifier: string } | undefined
|
||||
>;
|
||||
}
|
||||
| undefined
|
||||
>)
|
||||
: null;
|
||||
|
||||
if (!isDefined(objectViews)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: objectConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
this.logger.error(
|
||||
`Standard object "${objectNameSingular}" not found in workspace, this needs investigation, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate over view configs and find views by their universalIdentifier
|
||||
// (views have already been identified by the view identification command)
|
||||
for (const [viewName, viewConfig] of Object.entries(objectViews)) {
|
||||
if (!isDefined(viewConfig) || !isDefined(viewConfig.viewFields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatViewMaps,
|
||||
universalIdentifier: viewConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
this.logger.warn(
|
||||
`Standard view "${viewName}" not found for object "${flatObjectMetadata.nameSingular}", skipping view fields`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedFlatViewFields =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatView.viewFieldIds,
|
||||
flatEntityMaps: flatViewFieldMaps,
|
||||
});
|
||||
|
||||
// Iterate over expected view fields from config
|
||||
for (const [fieldName, viewFieldConfig] of Object.entries(
|
||||
viewConfig.viewFields,
|
||||
)) {
|
||||
if (!isDefined(viewFieldConfig)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldUniversalIdentifier =
|
||||
objectConfig.fields[fieldName as keyof typeof objectConfig.fields]
|
||||
?.universalIdentifier;
|
||||
|
||||
if (!isDefined(fieldUniversalIdentifier)) {
|
||||
this.logger.warn(
|
||||
`Field "${fieldName}" config not found for object "${flatObjectMetadata.nameSingular}", skipping view field`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewFieldUniversalIdentifier =
|
||||
viewConfig.viewFields[fieldName]?.universalIdentifier;
|
||||
|
||||
if (!isDefined(viewFieldUniversalIdentifier)) {
|
||||
this.logger.warn(
|
||||
`View field for field "${fieldName}" config not found for object "${flatObjectMetadata.nameSingular}", skipping view field`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the field metadata by universal identifier
|
||||
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier: fieldUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
this.logger.warn(
|
||||
`Field "${fieldName}" not found in workspace for object "${flatObjectMetadata.nameSingular}", skipping view field`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the existing view field that matches this field
|
||||
const matchingFlatViewField = relatedFlatViewFields.find(
|
||||
(viewField) => viewField.fieldMetadataId === flatFieldMetadata.id,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(matchingFlatViewField) ||
|
||||
isDefined(matchingFlatViewField.applicationId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
standardViewFieldUpdates.push({
|
||||
flatViewField: matchingFlatViewField,
|
||||
universalIdentifier: viewFieldUniversalIdentifier,
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
fieldName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions.length > 0) {
|
||||
this.logger.error(
|
||||
`Found ${exceptions.length} exception(s) while processing view field metadata for workspace ${workspaceId}. No updates will be applied.`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
flatViewField,
|
||||
exception,
|
||||
objectNameSingular,
|
||||
viewName,
|
||||
fieldName,
|
||||
} of exceptions) {
|
||||
this.logger.error(
|
||||
`Exception for view field "${fieldName}" on view "${viewName}" of object "${objectNameSingular}" (id=${flatViewField.id}): ${exception}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`,
|
||||
);
|
||||
}
|
||||
|
||||
const standardUpdates = standardViewFieldUpdates.map(
|
||||
({ flatViewField, universalIdentifier }) => ({
|
||||
id: flatViewField.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${standardUpdates.length} standard view field(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
flatViewField,
|
||||
universalIdentifier,
|
||||
objectNameSingular,
|
||||
viewName,
|
||||
fieldName,
|
||||
} of standardViewFieldUpdates) {
|
||||
this.logger.log(
|
||||
` - Standard view field "${fieldName}" on view "${viewName}" of object "${objectNameSingular}" (id=${flatViewField.id}) -> universalIdentifier=${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewFieldRepository.save(standardUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomViewFields({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomViewFields = await this.viewFieldRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
viewId: true,
|
||||
fieldMetadataId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomViewFields.map((viewFieldEntity) => ({
|
||||
id: viewFieldEntity.id,
|
||||
universalIdentifier: viewFieldEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom view field(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const viewFieldEntity of remainingCustomViewFields) {
|
||||
const flatView = flatViewMaps.byId[viewFieldEntity.viewId];
|
||||
const flatObjectMetadata = isDefined(flatView)
|
||||
? flatObjectMetadataMaps.byId[flatView.objectMetadataId]
|
||||
: undefined;
|
||||
const flatFieldMetadata =
|
||||
flatFieldMetadataMaps.byId[viewFieldEntity.fieldMetadataId];
|
||||
|
||||
this.logger.log(
|
||||
` - Custom view field for field "${flatFieldMetadata?.name ?? 'unknown'}" on view "${flatView?.name ?? 'unknown'}" of object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${viewFieldEntity.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewFieldRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-view-filter-metadata',
|
||||
description: 'Identify standard view filter metadata',
|
||||
})
|
||||
export class IdentifyViewFilterMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ViewFilterEntity)
|
||||
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard view filter metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatFieldMetadataMaps, flatViewMaps, flatViewFilterMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFilterMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await this.identifyStandardViewFilter({
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFilterMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomViewFilters({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('viewFilter');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatViewFilterMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardViewFilter({
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFilterMaps,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
flatViewFilterMaps: FlatEntityMaps<FlatViewFilter>;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatViewMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.task.views.assignedToMe.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
this.logger.warn(
|
||||
`Standard view "assignedToMe" not found for task object, skipping standard view filter identification`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.task.fields.assignee.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
this.logger.warn(
|
||||
`Field "assignee" not found for task object, skipping standard view filter identification`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedFlatViewFilters =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatView.viewFilterIds,
|
||||
flatEntityMaps: flatViewFilterMaps,
|
||||
});
|
||||
|
||||
const matchingFlatViewFilter = relatedFlatViewFilters.find(
|
||||
(viewFilter) => viewFilter.fieldMetadataId === flatFieldMetadata.id,
|
||||
);
|
||||
|
||||
if (!isDefined(matchingFlatViewFilter)) {
|
||||
this.logger.warn(
|
||||
`Standard view filter "assigneeIsMe" not found for view "assignedToMe" of task object, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(matchingFlatViewFilter.applicationId)) {
|
||||
this.logger.warn(
|
||||
`Standard view filter "assigneeIsMe" already has applicationId set, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
` - Standard view filter "assigneeIsMe" on view "assignedToMe" of object "task" (id=${matchingFlatViewFilter.id}) -> universalIdentifier=${STANDARD_OBJECTS.task.views.assignedToMe.viewFilters.assigneeIsMe.universalIdentifier}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewFilterRepository.save({
|
||||
id: matchingFlatViewFilter.id,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.task.views.assignedToMe.viewFilters.assigneeIsMe
|
||||
.universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomViewFilters({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomViewFilters = await this.viewFilterRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomViewFilters.map(
|
||||
(viewFilterEntity) => ({
|
||||
id: viewFilterEntity.id,
|
||||
universalIdentifier: viewFilterEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom view filter(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewFilterRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-340
@@ -1,340 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
const VIEW_GROUP_TO_FIELD_VALUE_MAPPING: Record<
|
||||
string,
|
||||
Record<string, Record<string, string>>
|
||||
> = {
|
||||
opportunity: {
|
||||
byStage: {
|
||||
new: 'NEW',
|
||||
screening: 'SCREENING',
|
||||
meeting: 'MEETING',
|
||||
proposal: 'PROPOSAL',
|
||||
customer: 'CUSTOMER',
|
||||
},
|
||||
},
|
||||
task: {
|
||||
assignedToMe: {
|
||||
todo: 'TODO',
|
||||
inProgress: 'IN_PROGRESS',
|
||||
done: 'DONE',
|
||||
empty: '',
|
||||
},
|
||||
byStatus: {
|
||||
todo: 'TODO',
|
||||
inProgress: 'IN_PROGRESS',
|
||||
done: 'DONE',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type StandardViewGroupUpdate = {
|
||||
flatViewGroup: FlatViewGroup;
|
||||
universalIdentifier: string;
|
||||
objectNameSingular: string;
|
||||
viewName: string;
|
||||
viewGroupName: string;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-view-group-metadata',
|
||||
description: 'Identify standard view group metadata',
|
||||
})
|
||||
export class IdentifyViewGroupMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ViewGroupEntity)
|
||||
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard view group metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatObjectMetadataMaps, flatViewMaps, flatViewGroupMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewGroupMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await this.identifyStandardViewGroups({
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomViewGroups({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('viewGroup');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatViewGroupMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardViewGroups({
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
flatViewGroupMaps: FlatEntityMaps<FlatViewGroup>;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const standardViewGroupUpdates: StandardViewGroupUpdate[] = [];
|
||||
|
||||
for (const [objectNameSingular, objectConfig] of Object.entries(
|
||||
STANDARD_OBJECTS,
|
||||
)) {
|
||||
const objectViews =
|
||||
'views' in objectConfig
|
||||
? (objectConfig.views as Record<
|
||||
string,
|
||||
| {
|
||||
universalIdentifier: string;
|
||||
viewGroups?: Record<
|
||||
string,
|
||||
{ universalIdentifier: string } | undefined
|
||||
>;
|
||||
}
|
||||
| undefined
|
||||
>)
|
||||
: null;
|
||||
|
||||
if (!isDefined(objectViews)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: objectConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
this.logger.error(
|
||||
`Standard object "${objectNameSingular}" not found in workspace, this needs investigation, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectViewGroupMapping =
|
||||
VIEW_GROUP_TO_FIELD_VALUE_MAPPING[objectNameSingular];
|
||||
|
||||
for (const [viewName, viewConfig] of Object.entries(objectViews)) {
|
||||
if (!isDefined(viewConfig) || !isDefined(viewConfig.viewGroups)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatViewMaps,
|
||||
universalIdentifier: viewConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
this.logger.warn(
|
||||
`Standard view "${viewName}" not found for object "${flatObjectMetadata.nameSingular}", skipping view groups`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedFlatViewGroups =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatView.viewGroupIds,
|
||||
flatEntityMaps: flatViewGroupMaps,
|
||||
});
|
||||
|
||||
const viewGroupMapping = objectViewGroupMapping?.[viewName];
|
||||
|
||||
for (const [viewGroupName, viewGroupConfig] of Object.entries(
|
||||
viewConfig.viewGroups,
|
||||
)) {
|
||||
if (!isDefined(viewGroupConfig)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldValue = viewGroupMapping?.[viewGroupName];
|
||||
|
||||
if (!isDefined(fieldValue) && fieldValue !== '') {
|
||||
this.logger.warn(
|
||||
`Field value mapping for view group "${viewGroupName}" not found for view "${viewName}" of object "${flatObjectMetadata.nameSingular}", skipping view group`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const matchingFlatViewGroup = relatedFlatViewGroups.find(
|
||||
(viewGroup) => viewGroup.fieldValue === fieldValue,
|
||||
);
|
||||
|
||||
if (!isDefined(matchingFlatViewGroup)) {
|
||||
this.logger.warn(
|
||||
`Standard view group "${viewGroupName}" with fieldValue="${fieldValue}" not found for view "${viewName}" of object "${flatObjectMetadata.nameSingular}", skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDefined(matchingFlatViewGroup.applicationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
standardViewGroupUpdates.push({
|
||||
flatViewGroup: matchingFlatViewGroup,
|
||||
universalIdentifier: viewGroupConfig.universalIdentifier,
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
viewGroupName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const standardUpdates = standardViewGroupUpdates.map(
|
||||
({ flatViewGroup, universalIdentifier }) => ({
|
||||
id: flatViewGroup.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${standardUpdates.length} standard view group(s) to update`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
flatViewGroup,
|
||||
universalIdentifier,
|
||||
objectNameSingular,
|
||||
viewName,
|
||||
viewGroupName,
|
||||
} of standardViewGroupUpdates) {
|
||||
this.logger.log(
|
||||
` - Standard view group "${viewGroupName}" on view "${viewName}" of object "${objectNameSingular}" (id=${flatViewGroup.id}) -> universalIdentifier=${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewGroupRepository.save(standardUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomViewGroups({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomViewGroups = await this.viewGroupRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
viewId: true,
|
||||
fieldValue: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomViewGroups.map((viewGroupEntity) => ({
|
||||
id: viewGroupEntity.id,
|
||||
universalIdentifier: viewGroupEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom view group(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const viewGroupEntity of remainingCustomViewGroups) {
|
||||
const flatView = flatViewMaps.byId[viewGroupEntity.viewId];
|
||||
const flatObjectMetadata = isDefined(flatView)
|
||||
? flatObjectMetadataMaps.byId[flatView.objectMetadataId]
|
||||
: undefined;
|
||||
|
||||
this.logger.log(
|
||||
` - Custom view group with fieldValue="${viewGroupEntity.fieldValue}" on view "${flatView?.name ?? 'unknown'}" of object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${viewGroupEntity.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewGroupRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-378
@@ -1,378 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { capitalize, isDefined, uncapitalize } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ALL_ENTITY_VIEW_NAME } from 'src/database/commands/upgrade-version-command/1-16/utils/compute-formatted-view-name.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
type StandardViewUpdate = {
|
||||
flatView: FlatView;
|
||||
universalIdentifier: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
type AllExceptions = 'unknown_standard_view';
|
||||
|
||||
type ViewMetadataException = {
|
||||
flatView: FlatView;
|
||||
exception: AllExceptions;
|
||||
objectNameSingular?: string;
|
||||
};
|
||||
|
||||
type DuplicateStandardView = {
|
||||
skippedFlatView: FlatView;
|
||||
keptFlatView: FlatView;
|
||||
objectNameSingular: string;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:identify-view-metadata',
|
||||
description: 'Identify standard view metadata',
|
||||
})
|
||||
export class IdentifyViewMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running identify standard view metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatObjectMetadataMaps, flatViewMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatViewMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
await this.identifyStandardViewsOrThrow({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
await this.identifyCustomViews({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
dryRun: options.dryRun ?? false,
|
||||
});
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('view');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatViewMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyStandardViewsOrThrow({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
twentyStandardApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
twentyStandardApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const standardViewUpdates: StandardViewUpdate[] = [];
|
||||
const exceptions: ViewMetadataException[] = [];
|
||||
|
||||
for (const [objectNameSingular, objectConfig] of Object.entries(
|
||||
STANDARD_OBJECTS,
|
||||
)) {
|
||||
const objectViews =
|
||||
'views' in objectConfig
|
||||
? (objectConfig.views as Record<
|
||||
string,
|
||||
{ universalIdentifier: string } | undefined
|
||||
>)
|
||||
: null;
|
||||
|
||||
if (!isDefined(objectViews)) {
|
||||
continue;
|
||||
}
|
||||
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: objectConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
this.logger.error(
|
||||
`Standard object "${objectNameSingular}" not found in workspace, this needs investigation, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedFlatViews = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: flatObjectMetadata.viewIds,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
for (const flatView of relatedFlatViews) {
|
||||
if (isDefined(flatView.applicationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// INDEX views -> forward to standard (if object has views config)
|
||||
if (
|
||||
flatView.key === ViewKey.INDEX &&
|
||||
flatView.name === ALL_ENTITY_VIEW_NAME
|
||||
) {
|
||||
const formattedViewName = `all${capitalize(flatObjectMetadata.namePlural)}`;
|
||||
|
||||
const viewConfig = objectViews[formattedViewName];
|
||||
const universalIdentifier = viewConfig?.universalIdentifier;
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
exceptions.push({
|
||||
exception: 'unknown_standard_view',
|
||||
flatView,
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
standardViewUpdates.push({
|
||||
flatView,
|
||||
universalIdentifier,
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Views with "All" name pattern but not INDEX -> forward to custom
|
||||
if (flatView.name.startsWith('All ')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remaining views (like assignedToMe, byStatus, etc.)
|
||||
const formattedViewName = uncapitalize(
|
||||
flatView.name.split(' ').map(capitalize).join(''),
|
||||
);
|
||||
|
||||
const viewConfig = objectViews[formattedViewName];
|
||||
const universalIdentifier = viewConfig?.universalIdentifier;
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
standardViewUpdates.push({
|
||||
flatView,
|
||||
universalIdentifier,
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions.length > 0) {
|
||||
this.logger.error(
|
||||
`Found ${exceptions.length} exception(s) while processing view metadata for workspace ${workspaceId}. No updates will be applied.`,
|
||||
);
|
||||
|
||||
for (const { flatView, exception, objectNameSingular } of exceptions) {
|
||||
this.logger.error(
|
||||
` - Exception for view "${flatView.name}" on object "${objectNameSingular ?? 'unknown'}" (id=${flatView.id}): ${exception}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Detect duplicate universalIdentifiers and keep the oldest view (created during workspace setup)
|
||||
const seenUniversalIdentifiers = new Map<string, StandardViewUpdate>();
|
||||
const duplicates: DuplicateStandardView[] = [];
|
||||
|
||||
for (const update of standardViewUpdates) {
|
||||
const existingUpdate = seenUniversalIdentifiers.get(
|
||||
update.universalIdentifier,
|
||||
);
|
||||
|
||||
if (isDefined(existingUpdate)) {
|
||||
const currentCreatedAt = new Date(update.flatView.createdAt).getTime();
|
||||
const existingCreatedAt = new Date(
|
||||
existingUpdate.flatView.createdAt,
|
||||
).getTime();
|
||||
|
||||
// Keep the oldest view, skip the newer one
|
||||
if (currentCreatedAt < existingCreatedAt) {
|
||||
// Current is older, replace existing
|
||||
duplicates.push({
|
||||
skippedFlatView: existingUpdate.flatView,
|
||||
keptFlatView: update.flatView,
|
||||
objectNameSingular: update.objectNameSingular,
|
||||
universalIdentifier: update.universalIdentifier,
|
||||
});
|
||||
seenUniversalIdentifiers.set(update.universalIdentifier, update);
|
||||
} else {
|
||||
// Existing is older, skip current
|
||||
duplicates.push({
|
||||
skippedFlatView: update.flatView,
|
||||
keptFlatView: existingUpdate.flatView,
|
||||
objectNameSingular: update.objectNameSingular,
|
||||
universalIdentifier: update.universalIdentifier,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
seenUniversalIdentifiers.set(update.universalIdentifier, update);
|
||||
}
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
this.logger.warn(
|
||||
`Found ${duplicates.length} duplicate standard view(s) for workspace ${workspaceId}. Keeping oldest, newer duplicates will be treated as custom views.`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
skippedFlatView,
|
||||
keptFlatView,
|
||||
objectNameSingular,
|
||||
universalIdentifier,
|
||||
} of duplicates) {
|
||||
this.logger.warn(
|
||||
` - Duplicate view "${skippedFlatView.name}" on object "${objectNameSingular}" (id=${skippedFlatView.id}, createdAt=${skippedFlatView.createdAt}) skipped in favor of older view (id=${keptFlatView.id}, createdAt=${keptFlatView.createdAt}) for universalIdentifier=${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deduplicatedStandardViewUpdates = Array.from(
|
||||
seenUniversalIdentifiers.values(),
|
||||
);
|
||||
|
||||
const standardUpdates = deduplicatedStandardViewUpdates.map(
|
||||
({ flatView, universalIdentifier }) => ({
|
||||
id: flatView.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${standardUpdates.length} standard view(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
flatView,
|
||||
universalIdentifier,
|
||||
objectNameSingular,
|
||||
} of deduplicatedStandardViewUpdates) {
|
||||
this.logger.log(
|
||||
` - Standard view "${flatView.name}" on object "${objectNameSingular}" (id=${flatView.id}) -> universalIdentifier=${universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewRepository.save(standardUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyCustomViews({
|
||||
workspaceId,
|
||||
flatObjectMetadataMaps,
|
||||
workspaceCustomApplicationId,
|
||||
dryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
workspaceCustomApplicationId: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const remainingCustomViews = await this.viewRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
name: true,
|
||||
objectMetadataId: true,
|
||||
isCustom: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const customUpdates = remainingCustomViews.map((viewEntity) => ({
|
||||
id: viewEntity.id,
|
||||
universalIdentifier: viewEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Found ${customUpdates.length} custom view(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const viewEntity of remainingCustomViews) {
|
||||
const flatObjectMetadata =
|
||||
flatObjectMetadataMaps.byId[viewEntity.objectMetadataId];
|
||||
|
||||
this.logger.log(
|
||||
` - Custom view "${viewEntity.name}" on object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${viewEntity.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.viewRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeAgentUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174274-makeAgentUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-agent-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on agent table',
|
||||
})
|
||||
export class MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeAgentUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-field-metadata-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on fieldMetadata table',
|
||||
})
|
||||
export class MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
`Rollbacking MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768830235328-makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-index-metadata-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on indexMetadata table',
|
||||
})
|
||||
export class MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768212224801-makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-object-metadata-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on objectMetadata table',
|
||||
})
|
||||
export class MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
`Roll backing MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768916632478-makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-remaining-entities-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on remaining entities (roleTarget, rowLevelPermissionPredicate, rowLevelPermissionPredicateGroup, viewFilterGroup, viewSort, cronTrigger, databaseEventTrigger, routeTrigger, logicFunction, skill, pageLayoutWidget, pageLayout, pageLayoutTab)',
|
||||
})
|
||||
export class MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeRoleUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174275-makeRoleUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-role-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on role table',
|
||||
})
|
||||
export class MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeRoleUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeViewFieldUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174272-makeViewFieldUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-view-field-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on viewField table',
|
||||
})
|
||||
export class MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeViewFieldUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeViewFilterUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174273-makeViewFilterUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-view-filter-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on viewFilter table',
|
||||
})
|
||||
export class MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeViewFilterUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-view-group-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on viewGroup table',
|
||||
})
|
||||
export class MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { makeViewUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174271-makeViewUniversalIdentifierAndApplicationIdNotNullable.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:make-view-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Make universalIdentifier and applicationId columns NOT NULL on view table',
|
||||
})
|
||||
export class MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
private hasRunOnce = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
if (this.hasRunOnce) {
|
||||
this.logger.warn(
|
||||
'Skipping has already been run once MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeViewUniversalIdentifierAndApplicationIdNotNullableQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-341
@@ -1,341 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { TASK_TARGET_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:update-task-on-delete-action',
|
||||
description:
|
||||
'Update task relation onDelete action to CASCADE and delete orphaned taskTarget records',
|
||||
})
|
||||
export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(UpdateTaskOnDeleteActionCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`Running UpdateTaskOnDeleteActionCommand for workspace ${workspaceId} (dryRun: ${isDryRun})`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[Phase 1] Starting task relation onDelete action update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.updateTaskRelationOnDeleteAction(workspaceId, isDryRun);
|
||||
|
||||
this.logger.log(
|
||||
`[Phase 1] Completed task relation onDelete action update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[Phase 2] Starting orphaned taskTarget cleanup for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.deleteOrphanedTaskTargets(workspaceId, isDryRun);
|
||||
|
||||
this.logger.log(
|
||||
`[Phase 2] Completed orphaned taskTarget cleanup for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`UpdateTaskOnDeleteActionCommand completed successfully for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async updateTaskRelationOnDeleteAction(
|
||||
workspaceId: string,
|
||||
isDryRun: boolean,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[Step 1/4] Fetching workspace cache for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
this.logger.log(
|
||||
`[Step 2/4] Looking for taskTarget object metadata in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const taskTargetObjectMetadata = Object.values(
|
||||
flatObjectMetadataMaps.byId,
|
||||
).find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata?.standardId === STANDARD_OBJECT_IDS.taskTarget,
|
||||
);
|
||||
|
||||
if (!isDefined(taskTargetObjectMetadata)) {
|
||||
this.logger.warn(
|
||||
`TaskTarget object metadata not found in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[Step 2/4] Found taskTarget object metadata with id ${taskTargetObjectMetadata.id} that has ${taskTargetObjectMetadata.fieldIds.length} field(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[Step 3/4] Looking for task field on taskTarget object in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const taskTargetFields = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: taskTargetObjectMetadata.fieldIds,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const taskField = taskTargetFields.find(
|
||||
(field) => field.standardId === TASK_TARGET_STANDARD_FIELD_IDS.task,
|
||||
);
|
||||
|
||||
if (!isDefined(taskField)) {
|
||||
this.logger.warn(
|
||||
`Task field not found on taskTarget object in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[Step 3/4] Found task field with id ${taskField.id} in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (taskField.type !== FieldMetadataType.RELATION) {
|
||||
this.logger.warn(
|
||||
`Task field is not a relation field (type: ${taskField.type}) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const taskFieldSettings =
|
||||
taskField.settings as FieldMetadataSettingsMapping['RELATION'];
|
||||
|
||||
if (taskFieldSettings?.onDelete === RelationOnDeleteAction.CASCADE) {
|
||||
this.logger.log(
|
||||
`[Step 4/4] Task relation already has CASCADE onDelete in workspace ${workspaceId}, skipping update`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[Step 4/4] Updating task relation onDelete from ${taskFieldSettings?.onDelete ?? 'undefined'} to CASCADE in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
const updatedSettings: FieldMetadataSettingsMapping['RELATION'] = {
|
||||
...taskFieldSettings,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
};
|
||||
|
||||
const updatedTaskField = {
|
||||
...taskField,
|
||||
settings: updatedSettings,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
`[Step 4/4] Calling validateBuildAndRunWorkspaceMigration for field ${taskField.id} in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [updatedTaskField],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.error(
|
||||
`[Step 4/4] Validation result: ${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
this.logger.error(
|
||||
`[Step 4/4] Failed to update task relation onDelete in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to update task relation onDelete in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[Step 4/4] Successfully updated task relation onDelete to CASCADE in workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Step 4/4] Failed to update task relation onDelete in workspace ${workspaceId}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Step 4/4] Field id: ${taskField.id}, current settings: ${JSON.stringify(taskFieldSettings)}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Step 4/4] Target settings: ${JSON.stringify(updatedSettings)}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Step 4/4] Error message: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Step 4/4] Error stack: ${error instanceof Error ? error.stack : 'No stack available'}`,
|
||||
);
|
||||
|
||||
this.logger.error(JSON.stringify(error, null, 2));
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
this.logger.log(
|
||||
`[Step 4/4] DRY RUN: Would update task relation onDelete to CASCADE in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteOrphanedTaskTargets(
|
||||
workspaceId: string,
|
||||
isDryRun: boolean,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 1/3] Getting taskTarget repository for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const taskTargetRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
'taskTarget',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 2/3] Searching for orphaned taskTarget records (taskId is null) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const orphanedTaskTargets = await taskTargetRepository.find({
|
||||
withDeleted: true,
|
||||
select: ['id'],
|
||||
where: {
|
||||
taskId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const orphanedCount = orphanedTaskTargets.length;
|
||||
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 2/3] Found ${orphanedCount} orphaned taskTarget record(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (orphanedCount === 0) {
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 3/3] No orphaned records to delete in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDryRun) {
|
||||
const orphanedIds = orphanedTaskTargets.map((record) => record.id);
|
||||
|
||||
const batchSize = 100;
|
||||
const totalBatches = Math.ceil(orphanedIds.length / batchSize);
|
||||
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 3/3] Deleting ${orphanedCount} orphaned taskTarget record(s) in ${totalBatches} batch(es) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (let i = 0; i < orphanedIds.length; i += batchSize) {
|
||||
const batch = orphanedIds.slice(i, i + batchSize);
|
||||
const batchNumber = Math.floor(i / batchSize) + 1;
|
||||
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 3/3] Deleting batch ${batchNumber}/${totalBatches} (${batch.length} records) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await taskTargetRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.whereInIds(batch)
|
||||
.execute();
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[Orphan cleanup 3/3] Failed to delete batch ${batchNumber}/${totalBatches} in workspace ${workspaceId}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Orphan cleanup 3/3] Batch record ids: ${JSON.stringify(batch)}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Orphan cleanup 3/3] Error message: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[Orphan cleanup 3/3] Error stack: ${error instanceof Error ? error.stack : 'No stack available'}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 3/3] Successfully deleted ${orphanedCount} orphaned taskTarget record(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`[Orphan cleanup 3/3] DRY RUN: Would delete ${orphanedCount} orphaned taskTarget record(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-opportunity-owner-field.command';
|
||||
import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command';
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-delete-all-files.command';
|
||||
import { FlushV2CacheAndIncrementMetadataVersionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-flush-v2-cache-and-increment-metadata-version.command';
|
||||
import { IdentifyAgentMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-agent-metadata.command';
|
||||
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
|
||||
import { IdentifyIndexMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-index-metadata.command';
|
||||
import { IdentifyObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-object-metadata.command';
|
||||
import { IdentifyRemainingEntitiesMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-remaining-entities-metadata.command';
|
||||
import { IdentifyRoleMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-role-metadata.command';
|
||||
import { IdentifyViewFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-field-metadata.command';
|
||||
import { IdentifyViewFilterMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-filter-metadata.command';
|
||||
import { IdentifyViewGroupMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command';
|
||||
import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.command';
|
||||
import { MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-agent-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-index-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-object-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-remaining-entities-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-role-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-field-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-filter-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
AgentEntity,
|
||||
FieldMetadataEntity,
|
||||
IndexMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
RoleEntity,
|
||||
ViewEntity,
|
||||
ViewFieldEntity,
|
||||
ViewFilterEntity,
|
||||
ViewGroupEntity,
|
||||
FileEntity,
|
||||
]),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheModule,
|
||||
FieldMetadataModule,
|
||||
ApplicationModule,
|
||||
GlobalWorkspaceDataSourceModule,
|
||||
TwentyStandardApplicationModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [
|
||||
UpdateTaskOnDeleteActionCommand,
|
||||
BackfillOpportunityOwnerFieldCommand,
|
||||
BackfillStandardPageLayoutsCommand,
|
||||
DeleteFileRecordsCommand,
|
||||
IdentifyAgentMetadataCommand,
|
||||
IdentifyFieldMetadataCommand,
|
||||
IdentifyIndexMetadataCommand,
|
||||
IdentifyObjectMetadataCommand,
|
||||
IdentifyRoleMetadataCommand,
|
||||
IdentifyViewMetadataCommand,
|
||||
IdentifyViewFieldMetadataCommand,
|
||||
IdentifyViewFilterMetadataCommand,
|
||||
MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
IdentifyViewGroupMetadataCommand,
|
||||
MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
IdentifyRemainingEntitiesMetadataCommand,
|
||||
FlushV2CacheAndIncrementMetadataVersionCommand,
|
||||
],
|
||||
exports: [
|
||||
UpdateTaskOnDeleteActionCommand,
|
||||
BackfillOpportunityOwnerFieldCommand,
|
||||
BackfillStandardPageLayoutsCommand,
|
||||
DeleteFileRecordsCommand,
|
||||
IdentifyAgentMetadataCommand,
|
||||
IdentifyFieldMetadataCommand,
|
||||
IdentifyIndexMetadataCommand,
|
||||
IdentifyObjectMetadataCommand,
|
||||
IdentifyRoleMetadataCommand,
|
||||
IdentifyViewMetadataCommand,
|
||||
IdentifyViewFieldMetadataCommand,
|
||||
IdentifyViewFilterMetadataCommand,
|
||||
MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
IdentifyViewGroupMetadataCommand,
|
||||
MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
IdentifyRemainingEntitiesMetadataCommand,
|
||||
FlushV2CacheAndIncrementMetadataVersionCommand,
|
||||
],
|
||||
})
|
||||
export class V1_16_UpgradeVersionCommandModule {}
|
||||
-299
@@ -1,299 +0,0 @@
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
|
||||
// Maps indexName to its related field universal identifiers for each standard object
|
||||
export const STANDARD_INDEX_FIELD_UNIVERSAL_IDENTIFIERS: Record<
|
||||
string,
|
||||
Record<string, string[]>
|
||||
> = {
|
||||
attachment: {
|
||||
taskIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetTask.universalIdentifier,
|
||||
],
|
||||
noteIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetNote.universalIdentifier,
|
||||
],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetPerson.universalIdentifier,
|
||||
],
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetCompany.universalIdentifier,
|
||||
],
|
||||
opportunityIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetOpportunity.universalIdentifier,
|
||||
],
|
||||
dashboardIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetDashboard.universalIdentifier,
|
||||
],
|
||||
workflowIdIndex: [
|
||||
STANDARD_OBJECTS.attachment.fields.targetWorkflow.universalIdentifier,
|
||||
],
|
||||
},
|
||||
blocklist: {
|
||||
workspaceMemberIdIndex: [
|
||||
STANDARD_OBJECTS.blocklist.fields.workspaceMember.universalIdentifier,
|
||||
],
|
||||
},
|
||||
calendarChannelEventAssociation: {
|
||||
calendarChannelIdIndex: [
|
||||
STANDARD_OBJECTS.calendarChannelEventAssociation.fields.calendarChannel
|
||||
.universalIdentifier,
|
||||
],
|
||||
calendarEventIdIndex: [
|
||||
STANDARD_OBJECTS.calendarChannelEventAssociation.fields.calendarEvent
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
calendarChannel: {
|
||||
connectedAccountIdIndex: [
|
||||
STANDARD_OBJECTS.calendarChannel.fields.connectedAccount
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
calendarEventParticipant: {
|
||||
calendarEventIdIndex: [
|
||||
STANDARD_OBJECTS.calendarEventParticipant.fields.calendarEvent
|
||||
.universalIdentifier,
|
||||
],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.calendarEventParticipant.fields.person
|
||||
.universalIdentifier,
|
||||
],
|
||||
workspaceMemberIdIndex: [
|
||||
STANDARD_OBJECTS.calendarEventParticipant.fields.workspaceMember
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
company: {
|
||||
accountOwnerIdIndex: [
|
||||
STANDARD_OBJECTS.company.fields.accountOwner.universalIdentifier,
|
||||
],
|
||||
domainNameUniqueIndex: [
|
||||
STANDARD_OBJECTS.company.fields.domainName.universalIdentifier,
|
||||
],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.company.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
connectedAccount: {
|
||||
accountOwnerIdIndex: [
|
||||
STANDARD_OBJECTS.connectedAccount.fields.accountOwner.universalIdentifier,
|
||||
],
|
||||
},
|
||||
dashboard: {
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.dashboard.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
favorite: {
|
||||
forWorkspaceMemberIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.forWorkspaceMember.universalIdentifier,
|
||||
],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.person.universalIdentifier,
|
||||
],
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.company.universalIdentifier,
|
||||
],
|
||||
favoriteFolderIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.favoriteFolder.universalIdentifier,
|
||||
],
|
||||
opportunityIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.opportunity.universalIdentifier,
|
||||
],
|
||||
workflowIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.workflow.universalIdentifier,
|
||||
],
|
||||
workflowVersionIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.workflowVersion.universalIdentifier,
|
||||
],
|
||||
workflowRunIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.workflowRun.universalIdentifier,
|
||||
],
|
||||
taskIdIndex: [STANDARD_OBJECTS.favorite.fields.task.universalIdentifier],
|
||||
noteIdIndex: [STANDARD_OBJECTS.favorite.fields.note.universalIdentifier],
|
||||
dashboardIdIndex: [
|
||||
STANDARD_OBJECTS.favorite.fields.dashboard.universalIdentifier,
|
||||
],
|
||||
},
|
||||
messageChannelMessageAssociation: {
|
||||
messageChannelIdIndex: [
|
||||
STANDARD_OBJECTS.messageChannelMessageAssociation.fields.messageChannel
|
||||
.universalIdentifier,
|
||||
],
|
||||
messageIdIndex: [
|
||||
STANDARD_OBJECTS.messageChannelMessageAssociation.fields.message
|
||||
.universalIdentifier,
|
||||
],
|
||||
messageChannelIdMessageIdUniqueIndex: [
|
||||
STANDARD_OBJECTS.messageChannelMessageAssociation.fields.messageChannel
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECTS.messageChannelMessageAssociation.fields.message
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
messageChannel: {
|
||||
connectedAccountIdIndex: [
|
||||
STANDARD_OBJECTS.messageChannel.fields.connectedAccount
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
messageFolder: {
|
||||
messageChannelIdIndex: [
|
||||
STANDARD_OBJECTS.messageFolder.fields.messageChannel.universalIdentifier,
|
||||
],
|
||||
},
|
||||
messageParticipant: {
|
||||
messageIdIndex: [
|
||||
STANDARD_OBJECTS.messageParticipant.fields.message.universalIdentifier,
|
||||
],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.messageParticipant.fields.person.universalIdentifier,
|
||||
],
|
||||
workspaceMemberIdIndex: [
|
||||
STANDARD_OBJECTS.messageParticipant.fields.workspaceMember
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
message: {
|
||||
messageThreadIdIndex: [
|
||||
STANDARD_OBJECTS.message.fields.messageThread.universalIdentifier,
|
||||
],
|
||||
},
|
||||
note: {
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.note.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
noteTarget: {
|
||||
noteIdIndex: [STANDARD_OBJECTS.noteTarget.fields.note.universalIdentifier],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.noteTarget.fields.person.universalIdentifier,
|
||||
],
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.noteTarget.fields.company.universalIdentifier,
|
||||
],
|
||||
opportunityIdIndex: [
|
||||
STANDARD_OBJECTS.noteTarget.fields.opportunity.universalIdentifier,
|
||||
],
|
||||
},
|
||||
opportunity: {
|
||||
pointOfContactIdIndex: [
|
||||
STANDARD_OBJECTS.opportunity.fields.pointOfContact.universalIdentifier,
|
||||
],
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.opportunity.fields.company.universalIdentifier,
|
||||
],
|
||||
stageIndex: [STANDARD_OBJECTS.opportunity.fields.stage.universalIdentifier],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.opportunity.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
person: {
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.person.fields.company.universalIdentifier,
|
||||
],
|
||||
emailsUniqueIndex: [
|
||||
STANDARD_OBJECTS.person.fields.emails.universalIdentifier,
|
||||
],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.person.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
task: {
|
||||
assigneeIdIndex: [
|
||||
STANDARD_OBJECTS.task.fields.assignee.universalIdentifier,
|
||||
],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.task.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
taskTarget: {
|
||||
taskIdIndex: [STANDARD_OBJECTS.taskTarget.fields.task.universalIdentifier],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.taskTarget.fields.person.universalIdentifier,
|
||||
],
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.taskTarget.fields.company.universalIdentifier,
|
||||
],
|
||||
opportunityIdIndex: [
|
||||
STANDARD_OBJECTS.taskTarget.fields.opportunity.universalIdentifier,
|
||||
],
|
||||
},
|
||||
timelineActivity: {
|
||||
workspaceMemberIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.workspaceMember
|
||||
.universalIdentifier,
|
||||
],
|
||||
personIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetPerson.universalIdentifier,
|
||||
],
|
||||
companyIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetCompany
|
||||
.universalIdentifier,
|
||||
],
|
||||
opportunityIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetOpportunity
|
||||
.universalIdentifier,
|
||||
],
|
||||
noteIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetNote.universalIdentifier,
|
||||
],
|
||||
taskIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetTask.universalIdentifier,
|
||||
],
|
||||
workflowIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflow
|
||||
.universalIdentifier,
|
||||
],
|
||||
workflowVersionIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowVersion
|
||||
.universalIdentifier,
|
||||
],
|
||||
workflowRunIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetWorkflowRun
|
||||
.universalIdentifier,
|
||||
],
|
||||
dashboardIdIndex: [
|
||||
STANDARD_OBJECTS.timelineActivity.fields.targetDashboard
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
workflow: {
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.workflow.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
workflowAutomatedTrigger: {
|
||||
workflowIdIndex: [
|
||||
STANDARD_OBJECTS.workflowAutomatedTrigger.fields.workflow
|
||||
.universalIdentifier,
|
||||
],
|
||||
},
|
||||
workflowRun: {
|
||||
workflowVersionIdIndex: [
|
||||
STANDARD_OBJECTS.workflowRun.fields.workflowVersion.universalIdentifier,
|
||||
],
|
||||
workflowIdIndex: [
|
||||
STANDARD_OBJECTS.workflowRun.fields.workflow.universalIdentifier,
|
||||
],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.workflowRun.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
workflowVersion: {
|
||||
workflowIdIndex: [
|
||||
STANDARD_OBJECTS.workflowVersion.fields.workflow.universalIdentifier,
|
||||
],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.workflowVersion.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
workspaceMember: {
|
||||
userEmailUniqueIndex: [
|
||||
STANDARD_OBJECTS.workspaceMember.fields.userEmail.universalIdentifier,
|
||||
],
|
||||
searchVectorGinIndex: [
|
||||
STANDARD_OBJECTS.workspaceMember.fields.searchVector.universalIdentifier,
|
||||
],
|
||||
},
|
||||
};
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { capitalize, uncapitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export const ALL_ENTITY_VIEW_NAME = 'All {objectLabelPlural}';
|
||||
|
||||
export const computeFormattedViewName = ({
|
||||
viewName,
|
||||
flatObjectMetadata,
|
||||
}: {
|
||||
viewName: string;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
}) =>
|
||||
viewName === ALL_ENTITY_VIEW_NAME
|
||||
? `all${capitalize(flatObjectMetadata.namePlural)}`
|
||||
: uncapitalize(viewName.split(' ').map(capitalize).join(''));
|
||||
-422
@@ -1,422 +0,0 @@
|
||||
import { isDefined, type RecordFilter } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
|
||||
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
|
||||
|
||||
type UpdateStandardPageLayoutWidgetsArgs = {
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatPageLayoutWidgetMaps: FlatEntityMaps<FlatPageLayoutWidget>;
|
||||
};
|
||||
|
||||
const findWidgetByUniversalIdentifierOrThrow = <
|
||||
T extends WidgetConfigurationType,
|
||||
>({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier,
|
||||
widgetDescription,
|
||||
expectedConfigurationType,
|
||||
}: {
|
||||
flatPageLayoutWidgetMaps: FlatEntityMaps<FlatPageLayoutWidget>;
|
||||
universalIdentifier: string;
|
||||
widgetDescription: string;
|
||||
expectedConfigurationType: T;
|
||||
}): FlatPageLayoutWidget<T> => {
|
||||
const flatPageLayoutWidget = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
if (
|
||||
!isFlatPageLayoutWidgetConfigurationOfType(
|
||||
flatPageLayoutWidget,
|
||||
expectedConfigurationType,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Widget ${widgetDescription} has unexpected configuration type ${flatPageLayoutWidget.configuration.configurationType}, expected ${expectedConfigurationType}`,
|
||||
);
|
||||
}
|
||||
|
||||
return flatPageLayoutWidget;
|
||||
};
|
||||
|
||||
// Widget updater: dealsByCompany (PIE_CHART)
|
||||
const updateDealsByCompanyWidget = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
const opportunityIdField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.id.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityCompanyField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.company.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityObject = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.opportunity.universalIdentifier,
|
||||
});
|
||||
|
||||
const widget = findWidgetByUniversalIdentifierOrThrow({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.tabs.tab1.widgets.dealsByCompany
|
||||
.universalIdentifier,
|
||||
widgetDescription: 'dealsByCompany',
|
||||
expectedConfigurationType: WidgetConfigurationType.PIE_CHART,
|
||||
});
|
||||
|
||||
const updatedWidget: FlatPageLayoutWidget<WidgetConfigurationType.PIE_CHART> =
|
||||
{
|
||||
...widget,
|
||||
objectMetadataId: opportunityObject.id,
|
||||
configuration: {
|
||||
...widget.configuration,
|
||||
aggregateFieldMetadataId: opportunityIdField.id,
|
||||
groupByFieldMetadataId: opportunityCompanyField.id,
|
||||
},
|
||||
};
|
||||
|
||||
return replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedWidget,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
};
|
||||
|
||||
// Widget updater: pipelineValueByStage (BAR_CHART)
|
||||
const updatePipelineValueByStageWidget = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
const opportunityAmountField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.amount.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityStageField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.stage.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityCompanyField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.company.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityObject = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.opportunity.universalIdentifier,
|
||||
});
|
||||
|
||||
const widget = findWidgetByUniversalIdentifierOrThrow({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.tabs.tab1.widgets
|
||||
.pipelineValueByStage.universalIdentifier,
|
||||
widgetDescription: 'pipelineValueByStage',
|
||||
expectedConfigurationType: WidgetConfigurationType.BAR_CHART,
|
||||
});
|
||||
|
||||
const updatedWidget: FlatPageLayoutWidget<WidgetConfigurationType.BAR_CHART> =
|
||||
{
|
||||
...widget,
|
||||
objectMetadataId: opportunityObject.id,
|
||||
configuration: {
|
||||
...widget.configuration,
|
||||
aggregateFieldMetadataId: opportunityAmountField.id,
|
||||
primaryAxisGroupByFieldMetadataId: opportunityStageField.id,
|
||||
secondaryAxisGroupByFieldMetadataId: opportunityCompanyField.id,
|
||||
},
|
||||
};
|
||||
|
||||
return replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedWidget,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
};
|
||||
|
||||
// Widget updater: revenueTimeline (LINE_CHART)
|
||||
const updateRevenueTimelineWidget = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
const opportunityAmountField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.amount.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityCloseDateField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.closeDate.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityObject = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.opportunity.universalIdentifier,
|
||||
});
|
||||
|
||||
const widget = findWidgetByUniversalIdentifierOrThrow({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.tabs.tab1.widgets.revenueTimeline
|
||||
.universalIdentifier,
|
||||
widgetDescription: 'revenueTimeline',
|
||||
expectedConfigurationType: WidgetConfigurationType.LINE_CHART,
|
||||
});
|
||||
|
||||
const updatedWidget: FlatPageLayoutWidget<WidgetConfigurationType.LINE_CHART> =
|
||||
{
|
||||
...widget,
|
||||
objectMetadataId: opportunityObject.id,
|
||||
configuration: {
|
||||
...widget.configuration,
|
||||
aggregateFieldMetadataId: opportunityAmountField.id,
|
||||
primaryAxisGroupByFieldMetadataId: opportunityCloseDateField.id,
|
||||
},
|
||||
};
|
||||
|
||||
return replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedWidget,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
};
|
||||
|
||||
// Widget updater: opportunitiesByOwner (BAR_CHART)
|
||||
const updateOpportunitiesByOwnerWidget = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
const opportunityIdField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.id.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityOwnerField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.owner.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityObject = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.opportunity.universalIdentifier,
|
||||
});
|
||||
|
||||
const widget = findWidgetByUniversalIdentifierOrThrow({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.tabs.tab1.widgets
|
||||
.opportunitiesByOwner.universalIdentifier,
|
||||
widgetDescription: 'opportunitiesByOwner',
|
||||
expectedConfigurationType: WidgetConfigurationType.BAR_CHART,
|
||||
});
|
||||
|
||||
const updatedWidget: FlatPageLayoutWidget<WidgetConfigurationType.BAR_CHART> =
|
||||
{
|
||||
...widget,
|
||||
objectMetadataId: opportunityObject.id,
|
||||
configuration: {
|
||||
...widget.configuration,
|
||||
aggregateFieldMetadataId: opportunityIdField.id,
|
||||
primaryAxisGroupByFieldMetadataId: opportunityOwnerField.id,
|
||||
secondaryAxisGroupByFieldMetadataId: opportunityOwnerField.id,
|
||||
},
|
||||
};
|
||||
|
||||
return replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedWidget,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
};
|
||||
|
||||
// Widget updater: dealsCreatedThisMonth (AGGREGATE_CHART)
|
||||
const updateDealsCreatedThisMonthWidget = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
const opportunityIdField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.id.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityCreatedAtField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.createdAt.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityObject = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.opportunity.universalIdentifier,
|
||||
});
|
||||
|
||||
const widget = findWidgetByUniversalIdentifierOrThrow({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.tabs.tab1.widgets
|
||||
.dealsCreatedThisMonth.universalIdentifier,
|
||||
widgetDescription: 'dealsCreatedThisMonth',
|
||||
expectedConfigurationType: WidgetConfigurationType.AGGREGATE_CHART,
|
||||
});
|
||||
|
||||
const updatedRecordFilters = widget.configuration.filter?.recordFilters?.map(
|
||||
(filter: RecordFilter) => ({
|
||||
...filter,
|
||||
fieldMetadataId: opportunityCreatedAtField.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const updatedWidget: FlatPageLayoutWidget<WidgetConfigurationType.AGGREGATE_CHART> =
|
||||
{
|
||||
...widget,
|
||||
objectMetadataId: opportunityObject.id,
|
||||
configuration: {
|
||||
...widget.configuration,
|
||||
aggregateFieldMetadataId: opportunityIdField.id,
|
||||
filter: isDefined(widget.configuration.filter)
|
||||
? {
|
||||
...widget.configuration.filter,
|
||||
recordFilters: updatedRecordFilters,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
return replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedWidget,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
};
|
||||
|
||||
// Widget updater: dealValueCreatedThisMonth (AGGREGATE_CHART)
|
||||
const updateDealValueCreatedThisMonthWidget = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
const opportunityAmountField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.amount.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityCreatedAtField = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.fields.createdAt.universalIdentifier,
|
||||
});
|
||||
|
||||
const opportunityObject = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.opportunity.universalIdentifier,
|
||||
});
|
||||
|
||||
const widget = findWidgetByUniversalIdentifierOrThrow({
|
||||
flatPageLayoutWidgetMaps,
|
||||
universalIdentifier:
|
||||
STANDARD_PAGE_LAYOUTS.myFirstDashboard.tabs.tab1.widgets
|
||||
.dealValueCreatedThisMonth.universalIdentifier,
|
||||
widgetDescription: 'dealValueCreatedThisMonth',
|
||||
expectedConfigurationType: WidgetConfigurationType.AGGREGATE_CHART,
|
||||
});
|
||||
|
||||
const updatedRecordFilters = widget.configuration.filter?.recordFilters?.map(
|
||||
(filter: RecordFilter) => ({
|
||||
...filter,
|
||||
fieldMetadataId: opportunityCreatedAtField.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const updatedWidget: FlatPageLayoutWidget<WidgetConfigurationType.AGGREGATE_CHART> =
|
||||
{
|
||||
...widget,
|
||||
objectMetadataId: opportunityObject.id,
|
||||
configuration: {
|
||||
...widget.configuration,
|
||||
aggregateFieldMetadataId: opportunityAmountField.id,
|
||||
filter: isDefined(widget.configuration.filter)
|
||||
? {
|
||||
...widget.configuration.filter,
|
||||
recordFilters: updatedRecordFilters,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
return replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedWidget,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateStandardPageLayoutWidgetsWithWorkspaceFieldIds = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: UpdateStandardPageLayoutWidgetsArgs): FlatEntityMaps<FlatPageLayoutWidget> => {
|
||||
let updatedMaps = flatPageLayoutWidgetMaps;
|
||||
|
||||
updatedMaps = updateDealsByCompanyWidget({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: updatedMaps,
|
||||
});
|
||||
|
||||
updatedMaps = updatePipelineValueByStageWidget({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: updatedMaps,
|
||||
});
|
||||
|
||||
updatedMaps = updateRevenueTimelineWidget({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: updatedMaps,
|
||||
});
|
||||
|
||||
updatedMaps = updateOpportunitiesByOwnerWidget({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: updatedMaps,
|
||||
});
|
||||
|
||||
updatedMaps = updateDealsCreatedThisMonthWidget({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: updatedMaps,
|
||||
});
|
||||
|
||||
updatedMaps = updateDealValueCreatedThisMonthWidget({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: updatedMaps,
|
||||
});
|
||||
|
||||
return updatedMaps;
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-delete-all-files.command';
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files.command';
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command';
|
||||
|
||||
-8
@@ -1,10 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module';
|
||||
import { V1_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-14/1-14-upgrade-version-command.module';
|
||||
import { V1_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-15/1-15-upgrade-version-command.module';
|
||||
import { V1_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module';
|
||||
import { V1_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-17/1-17-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';
|
||||
@@ -13,10 +9,6 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
V1_13_UpgradeVersionCommandModule,
|
||||
V1_14_UpgradeVersionCommandModule,
|
||||
V1_15_UpgradeVersionCommandModule,
|
||||
V1_16_UpgradeVersionCommandModule,
|
||||
V1_17_UpgradeVersionCommandModule,
|
||||
DataSourceModule,
|
||||
],
|
||||
|
||||
+3
-149
@@ -9,44 +9,7 @@ import {
|
||||
UpgradeCommandRunner,
|
||||
type VersionCommands,
|
||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command';
|
||||
import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command';
|
||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
||||
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
||||
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-index.command';
|
||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||
import { DeleteRemovedAgentsCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-delete-removed-agents.command';
|
||||
import { UpdateCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-update-created-by-enum.command';
|
||||
import { AddWorkspaceForeignKeysMigrationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-add-workspace-foreign-keys-migration.command';
|
||||
import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-backfill-updated-by-field.command';
|
||||
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
|
||||
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
|
||||
import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-opportunity-owner-field.command';
|
||||
import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command';
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-delete-all-files.command';
|
||||
import { FlushV2CacheAndIncrementMetadataVersionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-flush-v2-cache-and-increment-metadata-version.command';
|
||||
import { IdentifyAgentMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-agent-metadata.command';
|
||||
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
|
||||
import { IdentifyIndexMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-index-metadata.command';
|
||||
import { IdentifyObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-object-metadata.command';
|
||||
import { IdentifyRemainingEntitiesMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-remaining-entities-metadata.command';
|
||||
import { IdentifyRoleMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-role-metadata.command';
|
||||
import { IdentifyViewFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-field-metadata.command';
|
||||
import { IdentifyViewFilterMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-filter-metadata.command';
|
||||
import { IdentifyViewGroupMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command';
|
||||
import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.command';
|
||||
import { MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-agent-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-index-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-object-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-remaining-entities-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-role-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-field-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-filter-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command';
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files.command';
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command';
|
||||
@@ -69,53 +32,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
|
||||
// 1.13 Commands
|
||||
protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand,
|
||||
protected readonly deduplicateRoleTargetsCommand: DeduplicateRoleTargetsCommand,
|
||||
protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
protected readonly migrateStandardInvalidEntitiesCommand: MigrateStandardInvalidEntitiesCommand,
|
||||
protected readonly cleanEmptyStringNullInTextFieldsCommand: CleanEmptyStringNullInTextFieldsCommand,
|
||||
protected readonly renameIndexNameCommand: RenameIndexNameCommand,
|
||||
|
||||
// 1.14 Commands
|
||||
protected readonly updateCreatedByEnumCommand: UpdateCreatedByEnumCommand,
|
||||
protected readonly deleteRemovedAgentsCommand: DeleteRemovedAgentsCommand,
|
||||
|
||||
// 1.15 Commands
|
||||
protected readonly migratePageLayoutWidgetConfigurationCommand: MigratePageLayoutWidgetConfigurationCommand,
|
||||
protected readonly fixNanPositionValuesInNotesCommand: FixNanPositionValuesInNotesCommand,
|
||||
protected readonly backfillUpdatedByFieldCommand: BackfillUpdatedByFieldCommand,
|
||||
protected readonly addWorkspaceForeignKeysMigrationCommand: AddWorkspaceForeignKeysMigrationCommand,
|
||||
|
||||
// 1.16 Commands
|
||||
protected readonly updateTaskOnDeleteActionCommand: UpdateTaskOnDeleteActionCommand,
|
||||
protected readonly backfillOpportunityOwnerFieldCommand: BackfillOpportunityOwnerFieldCommand,
|
||||
protected readonly backfillStandardPageLayoutsCommand: BackfillStandardPageLayoutsCommand,
|
||||
protected readonly identifyAgentMetadataCommand: IdentifyAgentMetadataCommand,
|
||||
protected readonly identifyFieldMetadataCommand: IdentifyFieldMetadataCommand,
|
||||
protected readonly identifyIndexMetadataCommand: IdentifyIndexMetadataCommand,
|
||||
protected readonly identifyObjectMetadataCommand: IdentifyObjectMetadataCommand,
|
||||
protected readonly identifyRoleMetadataCommand: IdentifyRoleMetadataCommand,
|
||||
protected readonly identifyViewMetadataCommand: IdentifyViewMetadataCommand,
|
||||
protected readonly identifyViewFieldMetadataCommand: IdentifyViewFieldMetadataCommand,
|
||||
protected readonly identifyViewFilterMetadataCommand: IdentifyViewFilterMetadataCommand,
|
||||
protected readonly makeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly identifyViewGroupMetadataCommand: IdentifyViewGroupMetadataCommand,
|
||||
protected readonly makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly identifyRemainingEntitiesMetadataCommand: IdentifyRemainingEntitiesMetadataCommand,
|
||||
protected readonly makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly flushV2CacheAndIncrementMetadataVersionCommand: FlushV2CacheAndIncrementMetadataVersionCommand,
|
||||
protected readonly deleteFileRecordsCommand: DeleteFileRecordsCommand,
|
||||
|
||||
// 1.17 Commands
|
||||
protected readonly deleteFileRecordsCommand: DeleteFileRecordsCommand,
|
||||
protected readonly migrateAttachmentToMorphRelationsCommand: MigrateAttachmentToMorphRelationsCommand,
|
||||
protected readonly identifyWebhookMetadataCommand: IdentifyWebhookMetadataCommand,
|
||||
protected readonly makeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
@@ -128,67 +46,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
);
|
||||
|
||||
// Note: Required empty commands array to allow retrieving previous version
|
||||
const commands_1120: VersionCommands = [];
|
||||
|
||||
const commands_1130: VersionCommands = [
|
||||
this.migrateTimelineActivityToMorphRelationsCommand,
|
||||
this.deduplicateRoleTargetsCommand,
|
||||
this.updateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
this.backfillPageLayoutUniversalIdentifiersCommand,
|
||||
this.migrateStandardInvalidEntitiesCommand,
|
||||
this.cleanEmptyStringNullInTextFieldsCommand,
|
||||
this.renameIndexNameCommand,
|
||||
];
|
||||
|
||||
const commands_1140: VersionCommands = [
|
||||
this.updateCreatedByEnumCommand,
|
||||
this.deleteRemovedAgentsCommand,
|
||||
];
|
||||
|
||||
const commands_1150: VersionCommands = [
|
||||
this.migratePageLayoutWidgetConfigurationCommand,
|
||||
this.fixNanPositionValuesInNotesCommand,
|
||||
this.backfillUpdatedByFieldCommand,
|
||||
this.addWorkspaceForeignKeysMigrationCommand,
|
||||
];
|
||||
|
||||
const commands_1160: VersionCommands = [
|
||||
this.identifyAgentMetadataCommand,
|
||||
this.identifyFieldMetadataCommand,
|
||||
this.identifyObjectMetadataCommand,
|
||||
this.identifyRoleMetadataCommand,
|
||||
this.identifyViewMetadataCommand,
|
||||
this.identifyViewFieldMetadataCommand,
|
||||
this.identifyViewFilterMetadataCommand,
|
||||
this.identifyViewGroupMetadataCommand,
|
||||
this.identifyIndexMetadataCommand,
|
||||
this.backfillOpportunityOwnerFieldCommand,
|
||||
this.backfillStandardPageLayoutsCommand,
|
||||
this
|
||||
.makeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this
|
||||
.makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this.identifyRemainingEntitiesMetadataCommand,
|
||||
this
|
||||
.makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this.flushV2CacheAndIncrementMetadataVersionCommand,
|
||||
this.updateTaskOnDeleteActionCommand,
|
||||
this.deleteFileRecordsCommand,
|
||||
];
|
||||
const commands_1160: VersionCommands = [];
|
||||
|
||||
const commands_1170: VersionCommands = [
|
||||
this.migrateAttachmentToMorphRelationsCommand,
|
||||
@@ -199,10 +57,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
'1.12.0': commands_1120,
|
||||
'1.13.0': commands_1130,
|
||||
'1.14.0': commands_1140,
|
||||
'1.15.0': commands_1150,
|
||||
'1.16.0': commands_1160,
|
||||
'1.17.0': commands_1170,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user