Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79ce02346a | |||
| 1a23ff5d53 | |||
| 00beb00fd1 | |||
| c3d13f8e44 |
@@ -87,9 +87,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const isDirty =
|
||||
isDefined(settingsPersistedRole) &&
|
||||
!isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
|
||||
const isDirty = !isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
|
||||
|
||||
const handleCancel = () => {
|
||||
if (isCreateMode) {
|
||||
|
||||
+256
-251
@@ -2,68 +2,52 @@ 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 { computeFormattedViewName } from 'src/database/commands/upgrade-version-command/1-16/utils/compute-formatted-view-name.util';
|
||||
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/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 CustomViewFieldMetadata = {
|
||||
viewFieldEntity: ViewFieldEntity;
|
||||
fromStandard: boolean;
|
||||
};
|
||||
|
||||
type StandardViewFieldMetadata = {
|
||||
viewFieldEntity: ViewFieldEntity;
|
||||
type StandardViewFieldUpdate = {
|
||||
flatViewField: FlatViewField;
|
||||
universalIdentifier: string;
|
||||
objectNameSingular: string;
|
||||
viewName: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
type AllWarnings =
|
||||
| 'standard_object_has_no_standard_views'
|
||||
| 'unknown_view'
|
||||
| 'unknown_standard_view_field';
|
||||
|
||||
type ViewFieldMetadataWarning = {
|
||||
viewFieldEntity: ViewFieldEntity;
|
||||
warning: AllWarnings;
|
||||
objectNameSingular?: string;
|
||||
viewName?: string;
|
||||
fieldName?: string;
|
||||
};
|
||||
|
||||
type AllExceptions =
|
||||
| 'existing_universal_id_mismatch'
|
||||
| 'view_not_found'
|
||||
| 'object_not_found'
|
||||
| 'field_not_found';
|
||||
type AllExceptions = 'unknown_standard_view_field';
|
||||
|
||||
type ViewFieldMetadataException = {
|
||||
viewFieldEntity: ViewFieldEntity;
|
||||
flatViewField: FlatViewField;
|
||||
exception: AllExceptions;
|
||||
objectNameSingular?: string;
|
||||
viewName?: string;
|
||||
fieldName?: string;
|
||||
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 WorkspacesMigrationCommandRunner {
|
||||
export class IdentifyViewFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@@ -75,11 +59,7 @@ export class IdentifyViewFieldMetadataCommand extends WorkspacesMigrationCommand
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
WorkspaceActivationStatus.ONGOING_CREATION,
|
||||
]);
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
@@ -95,21 +75,12 @@ export class IdentifyViewFieldMetadataCommand extends WorkspacesMigrationCommand
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const allViewFieldEntities = await this.viewFieldRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
applicationId: true,
|
||||
viewId: true,
|
||||
fieldMetadataId: true,
|
||||
},
|
||||
where: {
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewMaps } =
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -117,84 +88,69 @@ export class IdentifyViewFieldMetadataCommand extends WorkspacesMigrationCommand
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const customViewFieldMetadataEntities: CustomViewFieldMetadata[] = [];
|
||||
const standardViewFieldMetadataEntities: StandardViewFieldMetadata[] = [];
|
||||
const warnings: ViewFieldMetadataWarning[] = [];
|
||||
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 viewFieldEntity of allViewFieldEntities) {
|
||||
const flatView = flatViewMaps.byId[viewFieldEntity.viewId];
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
exceptions.push({
|
||||
viewFieldEntity,
|
||||
exception: 'view_not_found',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flatView.isCustom) {
|
||||
customViewFieldMetadataEntities.push({
|
||||
viewFieldEntity,
|
||||
fromStandard: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatObjectMetadata =
|
||||
flatObjectMetadataMaps.byId[flatView.objectMetadataId];
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
exceptions.push({
|
||||
viewFieldEntity,
|
||||
exception: 'object_not_found',
|
||||
viewName: flatView.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatFieldMetadata =
|
||||
flatFieldMetadataMaps.byId[viewFieldEntity.fieldMetadataId];
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
exceptions.push({
|
||||
viewFieldEntity,
|
||||
exception: 'field_not_found',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
flatObjectMetadata.applicationId !== twentyStandardFlatApplication.id
|
||||
) {
|
||||
customViewFieldMetadataEntities.push({
|
||||
viewFieldEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectConfig =
|
||||
STANDARD_OBJECTS[
|
||||
flatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECTS
|
||||
];
|
||||
|
||||
if (!isDefined(objectConfig)) {
|
||||
exceptions.push({
|
||||
viewFieldEntity,
|
||||
exception: 'object_not_found',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [objectNameSingular, objectConfig] of Object.entries(
|
||||
STANDARD_OBJECTS,
|
||||
)) {
|
||||
const objectViews =
|
||||
'views' in objectConfig
|
||||
? (objectConfig.views as Record<
|
||||
@@ -211,97 +167,108 @@ export class IdentifyViewFieldMetadataCommand extends WorkspacesMigrationCommand
|
||||
: null;
|
||||
|
||||
if (!isDefined(objectViews)) {
|
||||
warnings.push({
|
||||
viewFieldEntity,
|
||||
warning: 'standard_object_has_no_standard_views',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
});
|
||||
customViewFieldMetadataEntities.push({
|
||||
viewFieldEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const formattedViewName = computeFormattedViewName({
|
||||
flatObjectMetadata,
|
||||
viewName: flatView.name,
|
||||
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: objectConfig.universalIdentifier,
|
||||
});
|
||||
const viewConfig = objectViews[formattedViewName];
|
||||
|
||||
if (!isDefined(viewConfig) || !isDefined(viewConfig.viewFields)) {
|
||||
warnings.push({
|
||||
viewFieldEntity,
|
||||
warning: 'unknown_view',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
});
|
||||
customViewFieldMetadataEntities.push({
|
||||
viewFieldEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewFieldConfig = viewConfig.viewFields[flatFieldMetadata.name];
|
||||
const universalIdentifier = viewFieldConfig?.universalIdentifier;
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
warnings.push({
|
||||
viewFieldEntity,
|
||||
warning: 'unknown_standard_view_field',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
fieldName: flatFieldMetadata.name,
|
||||
});
|
||||
customViewFieldMetadataEntities.push({
|
||||
viewFieldEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(viewFieldEntity.universalIdentifier) &&
|
||||
viewFieldEntity.universalIdentifier !== universalIdentifier
|
||||
) {
|
||||
exceptions.push({
|
||||
viewFieldEntity,
|
||||
exception: 'existing_universal_id_mismatch',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
viewName: flatView.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
standardViewFieldMetadataEntities.push({
|
||||
viewFieldEntity,
|
||||
universalIdentifier:
|
||||
viewFieldEntity.universalIdentifier ?? universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
const totalUpdates =
|
||||
customViewFieldMetadataEntities.length +
|
||||
standardViewFieldMetadataEntities.length;
|
||||
|
||||
if (warnings.length > 0) {
|
||||
this.logger.warn(
|
||||
`Found ${warnings.length} warning(s) while processing view field metadata for workspace ${workspaceId}. These view fields will become custom.`,
|
||||
);
|
||||
|
||||
for (const {
|
||||
viewFieldEntity,
|
||||
warning,
|
||||
objectNameSingular,
|
||||
viewName,
|
||||
fieldName,
|
||||
} of warnings) {
|
||||
this.logger.warn(
|
||||
`Warning for view field on object "${objectNameSingular ?? 'unknown'}" in view "${viewName ?? 'unknown'}" for field ${fieldName ?? 'unknown'} (id=${viewFieldEntity.id}): ${warning}`,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,14 +278,14 @@ export class IdentifyViewFieldMetadataCommand extends WorkspacesMigrationCommand
|
||||
);
|
||||
|
||||
for (const {
|
||||
viewFieldEntity,
|
||||
flatViewField,
|
||||
exception,
|
||||
objectNameSingular,
|
||||
viewName,
|
||||
fieldName,
|
||||
} of exceptions) {
|
||||
this.logger.error(
|
||||
`Exception for view field on object "${objectNameSingular ?? 'unknown'}" in view "${viewName ?? 'unknown'}" for field ${fieldName ?? 'unknown'} (id=${viewFieldEntity.id}): ${exception}`,
|
||||
`Exception for view field "${fieldName}" on view "${viewName}" of object "${objectNameSingular}" (id=${flatViewField.id}): ${exception}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,52 +294,90 @@ export class IdentifyViewFieldMetadataCommand extends WorkspacesMigrationCommand
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully validated ${totalUpdates}/${allViewFieldEntities.length} view field metadata update(s) for workspace ${workspaceId} (${customViewFieldMetadataEntities.length} custom, ${standardViewFieldMetadataEntities.length} standard)`,
|
||||
const standardUpdates = standardViewFieldUpdates.map(
|
||||
({ flatViewField, universalIdentifier }) => ({
|
||||
id: flatViewField.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
const customUpdates = customViewFieldMetadataEntities.map(
|
||||
({ viewFieldEntity }) => ({
|
||||
id: viewFieldEntity.id,
|
||||
universalIdentifier: viewFieldEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
this.logger.log(
|
||||
`Found ${standardUpdates.length} standard view field(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const standardUpdates = standardViewFieldMetadataEntities.map(
|
||||
({ viewFieldEntity, universalIdentifier }) => ({
|
||||
id: viewFieldEntity.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
}),
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.viewFieldRepository.save([
|
||||
...customUpdates,
|
||||
...standardUpdates,
|
||||
]);
|
||||
if (!dryRun) {
|
||||
await this.viewFieldRepository.save(standardUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('viewField');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
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(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
` - Custom view field for field "${flatFieldMetadata?.name ?? 'unknown'}" on view "${flatView?.name ?? 'unknown'}" of object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${viewFieldEntity.id})`,
|
||||
);
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatViewFieldMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Applied ${totalUpdates} view field metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Dry run: would apply ${totalUpdates} view field metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
if (!dryRun) {
|
||||
await this.viewFieldRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+297
-210
@@ -1,61 +1,56 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { capitalize, isDefined, uncapitalize } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
RunOnWorkspaceArgs,
|
||||
WorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { computeFormattedViewName } from 'src/database/commands/upgrade-version-command/1-16/utils/compute-formatted-view-name.util';
|
||||
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/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 CustomViewMetadata = {
|
||||
viewEntity: ViewEntity;
|
||||
fromStandard: boolean;
|
||||
};
|
||||
|
||||
type StandardViewMetadata = {
|
||||
viewEntity: ViewEntity;
|
||||
type StandardViewUpdate = {
|
||||
flatView: FlatView;
|
||||
universalIdentifier: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
type AllWarnings = 'unknown_object';
|
||||
|
||||
type ViewMetadataWarning = {
|
||||
viewEntity: ViewEntity;
|
||||
warning: AllWarnings;
|
||||
objectNameSingular?: string;
|
||||
};
|
||||
|
||||
type AllExceptions =
|
||||
| 'existing_universal_id_mismatch'
|
||||
| 'not_found_object'
|
||||
| 'unknown_standard_view_name';
|
||||
type AllExceptions = 'unknown_standard_view';
|
||||
|
||||
type ViewMetadataException = {
|
||||
viewEntity: ViewEntity;
|
||||
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 WorkspacesMigrationCommandRunner {
|
||||
export class IdentifyViewMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@@ -67,11 +62,7 @@ export class IdentifyViewMetadataCommand extends WorkspacesMigrationCommandRunne
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
WorkspaceActivationStatus.ONGOING_CREATION,
|
||||
]);
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
@@ -87,7 +78,265 @@ export class IdentifyViewMetadataCommand extends WorkspacesMigrationCommandRunne
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const allViewEntities = await this.viewRepository.find({
|
||||
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,
|
||||
@@ -100,192 +349,30 @@ export class IdentifyViewMetadataCommand extends WorkspacesMigrationCommandRunne
|
||||
workspaceId,
|
||||
applicationId: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
const customUpdates = remainingCustomViews.map((viewEntity) => ({
|
||||
id: viewEntity.id,
|
||||
universalIdentifier: viewEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
}));
|
||||
|
||||
const customViewMetadataEntities: CustomViewMetadata[] = [];
|
||||
const standardViewMetadataEntities: StandardViewMetadata[] = [];
|
||||
const warnings: ViewMetadataWarning[] = [];
|
||||
const exceptions: ViewMetadataException[] = [];
|
||||
|
||||
for (const viewEntity of allViewEntities) {
|
||||
// TODO double check that index view are not custom clearly not sure sure about that
|
||||
if (viewEntity.isCustom) {
|
||||
customViewMetadataEntities.push({
|
||||
viewEntity,
|
||||
fromStandard: false,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
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];
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
exceptions.push({
|
||||
viewEntity,
|
||||
exception: 'not_found_object',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectConfig =
|
||||
STANDARD_OBJECTS[
|
||||
flatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECTS
|
||||
];
|
||||
|
||||
if (!isDefined(objectConfig)) {
|
||||
warnings.push({
|
||||
viewEntity,
|
||||
warning: 'unknown_object',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
});
|
||||
customViewMetadataEntities.push({
|
||||
viewEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectViews =
|
||||
'views' in objectConfig
|
||||
? (objectConfig.views as Record<
|
||||
string,
|
||||
{ universalIdentifier: string } | undefined
|
||||
>)
|
||||
: null;
|
||||
|
||||
if (!isDefined(objectViews)) {
|
||||
warnings.push({
|
||||
viewEntity,
|
||||
warning: 'unknown_object',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
});
|
||||
customViewMetadataEntities.push({
|
||||
viewEntity,
|
||||
fromStandard: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const formattedViewName = computeFormattedViewName({
|
||||
viewName: viewEntity.name,
|
||||
flatObjectMetadata,
|
||||
});
|
||||
|
||||
this.logger.log(formattedViewName);
|
||||
const viewConfig = objectViews[formattedViewName];
|
||||
const universalIdentifier = viewConfig?.universalIdentifier;
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
exceptions.push({
|
||||
viewEntity,
|
||||
exception: 'unknown_standard_view_name',
|
||||
objectNameSingular: flatObjectMetadata.nameSingular,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(viewEntity.universalIdentifier) &&
|
||||
viewEntity.universalIdentifier !== universalIdentifier
|
||||
) {
|
||||
exceptions.push({
|
||||
viewEntity,
|
||||
exception: 'existing_universal_id_mismatch',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
standardViewMetadataEntities.push({
|
||||
viewEntity,
|
||||
universalIdentifier:
|
||||
viewEntity.universalIdentifier ?? universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
const totalUpdates =
|
||||
customViewMetadataEntities.length + standardViewMetadataEntities.length;
|
||||
|
||||
if (warnings.length > 0) {
|
||||
this.logger.warn(
|
||||
`Found ${warnings.length} warning(s) while processing view metadata for workspace ${workspaceId}. These views will become custom.`,
|
||||
);
|
||||
|
||||
for (const { viewEntity, warning, objectNameSingular } of warnings) {
|
||||
this.logger.warn(
|
||||
`Warning for view "${viewEntity.name}" on object "${objectNameSingular ?? 'unknown'}" (id=${viewEntity.id}): ${warning}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 { viewEntity, exception, objectNameSingular } of exceptions) {
|
||||
this.logger.error(
|
||||
`Exception for view "${viewEntity.name}" on object "${objectNameSingular ?? 'unknown'}" (id=${viewEntity.id}): ${exception}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`,
|
||||
this.logger.log(
|
||||
` - Custom view "${viewEntity.name}" on object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${viewEntity.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully validated ${totalUpdates}/${allViewEntities.length} view metadata update(s) for workspace ${workspaceId} (${customViewMetadataEntities.length} custom, ${standardViewMetadataEntities.length} standard)`,
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
const customUpdates = customViewMetadataEntities.map(
|
||||
({ viewEntity }) => ({
|
||||
id: viewEntity.id,
|
||||
universalIdentifier: viewEntity.universalIdentifier ?? v4(),
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const standardUpdates = standardViewMetadataEntities.map(
|
||||
({ viewEntity, universalIdentifier }) => ({
|
||||
id: viewEntity.id,
|
||||
universalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.viewRepository.save([...customUpdates, ...standardUpdates]);
|
||||
|
||||
const relatedMetadataNames = getMetadataRelatedMetadataNames('view');
|
||||
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
|
||||
);
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatViewMaps',
|
||||
...relatedCacheKeysToInvalidate,
|
||||
]);
|
||||
|
||||
this.logger.log(
|
||||
`Applied ${totalUpdates} view metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Dry run: would apply ${totalUpdates} view metadata update(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
if (!dryRun) {
|
||||
await this.viewRepository.save(customUpdates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-265
@@ -1,265 +0,0 @@
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-actions-report.constant';
|
||||
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
import { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util';
|
||||
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
|
||||
|
||||
type DeleteAggregationTestCase = EachTestingContext<{
|
||||
input: OrchestratorActionsReport;
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: Record<string, number>;
|
||||
expectDeleteObjectActionPerObjectMetadataId: Record<string, number>;
|
||||
};
|
||||
}>;
|
||||
|
||||
describe('aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions', () => {
|
||||
const testCases: DeleteAggregationTestCase[] = [
|
||||
{
|
||||
title: 'should remove field actions when parent object is being deleted',
|
||||
context: {
|
||||
input: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-1',
|
||||
} satisfies DeleteObjectAction,
|
||||
],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-2',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
],
|
||||
},
|
||||
} satisfies OrchestratorActionsReport,
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: {},
|
||||
expectDeleteObjectActionPerObjectMetadataId: {
|
||||
'object-1': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should keep field actions when no parent object is being deleted',
|
||||
context: {
|
||||
input: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
],
|
||||
},
|
||||
} satisfies OrchestratorActionsReport,
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: {
|
||||
'object-1': 1,
|
||||
},
|
||||
expectDeleteObjectActionPerObjectMetadataId: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should handle mixed scenario with some fields removed and some kept',
|
||||
context: {
|
||||
input: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-1',
|
||||
} satisfies DeleteObjectAction,
|
||||
],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-2',
|
||||
objectMetadataId: 'object-2',
|
||||
} satisfies DeleteFieldAction,
|
||||
],
|
||||
},
|
||||
} satisfies OrchestratorActionsReport,
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: {
|
||||
'object-2': 1,
|
||||
},
|
||||
expectDeleteObjectActionPerObjectMetadataId: {
|
||||
'object-1': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle multiple objects with mixed field deletions',
|
||||
context: {
|
||||
input: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-1',
|
||||
} satisfies DeleteObjectAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-2',
|
||||
} satisfies DeleteObjectAction,
|
||||
],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-2',
|
||||
objectMetadataId: 'object-2',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-3',
|
||||
objectMetadataId: 'object-3',
|
||||
} satisfies DeleteFieldAction,
|
||||
],
|
||||
},
|
||||
} satisfies OrchestratorActionsReport,
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: {
|
||||
'object-3': 1,
|
||||
},
|
||||
expectDeleteObjectActionPerObjectMetadataId: {
|
||||
'object-1': 1,
|
||||
'object-2': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle empty actions report',
|
||||
context: {
|
||||
input: createEmptyOrchestratorActionsReport(),
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: {},
|
||||
expectDeleteObjectActionPerObjectMetadataId: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(eachTestingContextFilter(testCases))(
|
||||
'$title',
|
||||
({ context: { input, expected } }) => {
|
||||
const result =
|
||||
aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions({
|
||||
orchestratorActionsReport: input,
|
||||
});
|
||||
|
||||
const fieldActions = result.fieldMetadata.delete as DeleteFieldAction[];
|
||||
const fieldActionCounts = fieldActions.reduce(
|
||||
(acc, action) => {
|
||||
acc[action.objectMetadataId] =
|
||||
(acc[action.objectMetadataId] || 0) + 1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
const objectActions = result.objectMetadata
|
||||
.delete as DeleteObjectAction[];
|
||||
const objectActionCounts = objectActions.reduce(
|
||||
(acc, action) => {
|
||||
acc[action.entityId] = (acc[action.entityId] || 0) + 1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
Object.entries(
|
||||
expected.expectDeleteFieldActionPerObjectMetadataId,
|
||||
).forEach(([objectId, expectedCount]) => {
|
||||
expect(fieldActionCounts[objectId]).toBe(expectedCount);
|
||||
});
|
||||
|
||||
Object.entries(
|
||||
expected.expectDeleteObjectActionPerObjectMetadataId,
|
||||
).forEach(([objectId, expectedCount]) => {
|
||||
expect(objectActionCounts[objectId]).toBe(expectedCount);
|
||||
});
|
||||
|
||||
// Check total counts
|
||||
const expectedTotalFieldActions = Object.values(
|
||||
expected.expectDeleteFieldActionPerObjectMetadataId,
|
||||
).reduce((sum, count) => sum + count, 0);
|
||||
const expectedTotalObjectActions = Object.values(
|
||||
expected.expectDeleteObjectActionPerObjectMetadataId,
|
||||
).reduce((sum, count) => sum + count, 0);
|
||||
|
||||
expect(fieldActions).toHaveLength(expectedTotalFieldActions);
|
||||
expect(objectActions).toHaveLength(expectedTotalObjectActions);
|
||||
},
|
||||
);
|
||||
});
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
|
||||
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
|
||||
|
||||
type AggregatedActions = {
|
||||
deleteFieldActionByFieldMetadataId: Record<string, DeleteFieldAction>;
|
||||
};
|
||||
export const aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions =
|
||||
({
|
||||
orchestratorActionsReport,
|
||||
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
|
||||
const deleteObjectActionByObjectMetadataId = (
|
||||
orchestratorActionsReport.objectMetadata.delete as DeleteObjectAction[]
|
||||
).reduce<Record<string, DeleteObjectAction>>(
|
||||
(acc, deleteObjectAction) => ({
|
||||
...acc,
|
||||
[deleteObjectAction.entityId]: deleteObjectAction,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const initialAccumulator: AggregatedActions = {
|
||||
deleteFieldActionByFieldMetadataId: {},
|
||||
};
|
||||
|
||||
const { deleteFieldActionByFieldMetadataId } = (
|
||||
orchestratorActionsReport.fieldMetadata.delete as DeleteFieldAction[]
|
||||
).reduce<AggregatedActions>(
|
||||
({ deleteFieldActionByFieldMetadataId }, deleteFieldAction) => {
|
||||
const fieldParentObjectDeleteObjectAction =
|
||||
deleteObjectActionByObjectMetadataId[
|
||||
deleteFieldAction.objectMetadataId
|
||||
];
|
||||
|
||||
if (isDefined(fieldParentObjectDeleteObjectAction)) {
|
||||
return {
|
||||
deleteFieldActionByFieldMetadataId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deleteFieldActionByFieldMetadataId: {
|
||||
...deleteFieldActionByFieldMetadataId,
|
||||
[deleteFieldAction.entityId]: deleteFieldAction,
|
||||
},
|
||||
};
|
||||
},
|
||||
initialAccumulator,
|
||||
);
|
||||
|
||||
return {
|
||||
...orchestratorActionsReport,
|
||||
fieldMetadata: {
|
||||
...orchestratorActionsReport.fieldMetadata,
|
||||
delete: Object.values(deleteFieldActionByFieldMetadataId),
|
||||
},
|
||||
};
|
||||
};
|
||||
-2
@@ -1,6 +1,5 @@
|
||||
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
|
||||
import { aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util';
|
||||
import { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util';
|
||||
import { aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util';
|
||||
|
||||
export const aggregateOrchestratorActionsReport = ({
|
||||
@@ -9,7 +8,6 @@ export const aggregateOrchestratorActionsReport = ({
|
||||
}: AggregateOrchestratorActionsReportArgs) => {
|
||||
const aggregatedOrchestratorActionsReport = [
|
||||
aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions,
|
||||
aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions,
|
||||
aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions,
|
||||
].reduce(
|
||||
(currentOrchestratorActionsReport, aggregator) =>
|
||||
|
||||
Reference in New Issue
Block a user