Identify standard view fields and views (#17118)

# Introduction
Related https://github.com/twentyhq/core-team-issues/issues/1989

1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous `standardId` or `isCustom`

## Test
Both tested on prod extract
Some view field are set as non custom is prod whereas they should for
several manually handle-able workspace amount
This commit is contained in:
Paul Rastoin
2026-01-14 15:15:07 +01:00
committed by GitHub
parent f70b171843
commit 2c24180b44
15 changed files with 1051 additions and 16 deletions
@@ -216,17 +216,17 @@ export class IdentifyFieldMetadataCommand extends WorkspacesMigrationCommandRunn
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const cacheKeysToInvalidate = relatedMetadataNames.map(
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
getMetadataFlatEntityMapsKey,
);
this.logger.log(
`Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`,
);
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
cacheKeysToInvalidate,
`Invalidating caches: flatFieldMetadataMaps ${relatedCacheKeysToInvalidate.join(' ')}`,
);
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatFieldMetadataMaps',
...relatedCacheKeysToInvalidate,
]);
this.logger.log(
`Applied ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`,
@@ -208,17 +208,17 @@ export class IdentifyObjectMetadataCommand extends WorkspacesMigrationCommandRun
const relatedMetadataNames =
getMetadataRelatedMetadataNames('objectMetadata');
const cacheKeysToInvalidate = relatedMetadataNames.map(
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
getMetadataFlatEntityMapsKey,
);
this.logger.log(
`Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`,
);
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
cacheKeysToInvalidate,
`Invalidating caches: flatObjectMetadataMaps ${relatedCacheKeysToInvalidate.join(' ')}`,
);
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatObjectMetadataMaps',
...relatedCacheKeysToInvalidate,
]);
this.logger.log(
`Applied ${totalUpdates} object metadata update(s) for workspace ${workspaceId}`,
@@ -0,0 +1,378 @@
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 { 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 { 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 { 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;
universalIdentifier: 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 ViewFieldMetadataException = {
viewFieldEntity: ViewFieldEntity;
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 WorkspacesMigrationCommandRunner {
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, [
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
WorkspaceActivationStatus.ONGOING_CREATION,
]);
}
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 allViewFieldEntities = await this.viewFieldRepository.find({
select: {
id: true,
universalIdentifier: true,
applicationId: true,
viewId: true,
fieldMetadataId: true,
},
where: {
workspaceId,
applicationId: IsNull(),
},
});
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatViewMaps',
],
},
);
const customViewFieldMetadataEntities: CustomViewFieldMetadata[] = [];
const standardViewFieldMetadataEntities: StandardViewFieldMetadata[] = [];
const warnings: ViewFieldMetadataWarning[] = [];
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;
}
const objectViews =
'views' in objectConfig
? (objectConfig.views as Record<
string,
| {
universalIdentifier: string;
viewFields?: Record<
string,
{ universalIdentifier: string } | undefined
>;
}
| undefined
>)
: 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 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 (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 {
viewFieldEntity,
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}`,
);
}
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}/${allViewFieldEntities.length} view field metadata update(s) for workspace ${workspaceId} (${customViewFieldMetadataEntities.length} custom, ${standardViewFieldMetadataEntities.length} standard)`,
);
if (!options.dryRun) {
const customUpdates = customViewFieldMetadataEntities.map(
({ viewFieldEntity }) => ({
id: viewFieldEntity.id,
universalIdentifier: viewFieldEntity.universalIdentifier ?? v4(),
applicationId: workspaceCustomFlatApplication.id,
}),
);
const standardUpdates = standardViewFieldMetadataEntities.map(
({ viewFieldEntity, universalIdentifier }) => ({
id: viewFieldEntity.id,
universalIdentifier,
applicationId: twentyStandardFlatApplication.id,
}),
);
await this.viewFieldRepository.save([
...customUpdates,
...standardUpdates,
]);
const relatedMetadataNames = getMetadataRelatedMetadataNames('viewField');
const relatedCacheKeysToInvalidate = relatedMetadataNames.map(
getMetadataFlatEntityMapsKey,
);
this.logger.log(
`Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`,
);
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}`,
);
}
}
}
@@ -0,0 +1,291 @@
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 { 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 { 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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.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 CustomViewMetadata = {
viewEntity: ViewEntity;
fromStandard: boolean;
};
type StandardViewMetadata = {
viewEntity: ViewEntity;
universalIdentifier: 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 ViewMetadataException = {
viewEntity: ViewEntity;
exception: AllExceptions;
objectNameSingular?: string;
};
@Command({
name: 'upgrade:1-16:identify-view-metadata',
description: 'Identify standard view metadata',
})
export class IdentifyViewMetadataCommand extends WorkspacesMigrationCommandRunner {
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, [
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
WorkspaceActivationStatus.ONGOING_CREATION,
]);
}
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 allViewEntities = await this.viewRepository.find({
select: {
id: true,
universalIdentifier: true,
applicationId: true,
name: true,
objectMetadataId: true,
isCustom: true,
},
where: {
workspaceId,
applicationId: IsNull(),
},
});
const { flatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
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;
}
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(
`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}`,
);
}
}
}
@@ -0,0 +1,71 @@
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();
}
}
}
@@ -0,0 +1,71 @@
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();
}
}
}
@@ -5,15 +5,22 @@ import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgr
import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command';
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
import { IdentifyObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-object-metadata.command';
import { IdentifyViewFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-field-metadata.command';
import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.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 { 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 { 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 { 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 { 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 { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { 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';
@@ -25,6 +32,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
WorkspaceEntity,
FieldMetadataEntity,
ObjectMetadataEntity,
ViewEntity,
ViewFieldEntity,
]),
DataSourceModule,
WorkspaceCacheModule,
@@ -33,6 +42,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
GlobalWorkspaceDataSourceModule,
TwentyStandardApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [
UpdateTaskOnDeleteActionCommand,
@@ -40,8 +50,12 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
BackfillStandardPageLayoutsCommand,
IdentifyFieldMetadataCommand,
IdentifyObjectMetadataCommand,
IdentifyViewMetadataCommand,
IdentifyViewFieldMetadataCommand,
MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
],
exports: [
UpdateTaskOnDeleteActionCommand,
@@ -49,8 +63,12 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
BackfillStandardPageLayoutsCommand,
IdentifyFieldMetadataCommand,
IdentifyObjectMetadataCommand,
IdentifyViewMetadataCommand,
IdentifyViewFieldMetadataCommand,
MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
],
})
export class V1_16_UpgradeVersionCommandModule {}
@@ -0,0 +1,16 @@
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(''));
@@ -26,8 +26,12 @@ import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgr
import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command';
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
import { IdentifyObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-object-metadata.command';
import { IdentifyViewFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-field-metadata.command';
import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.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 { 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 { 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 { 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -73,8 +77,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillStandardPageLayoutsCommand: BackfillStandardPageLayoutsCommand,
protected readonly identifyFieldMetadataCommand: IdentifyFieldMetadataCommand,
protected readonly identifyObjectMetadataCommand: IdentifyObjectMetadataCommand,
protected readonly identifyViewMetadataCommand: IdentifyViewMetadataCommand,
protected readonly identifyViewFieldMetadataCommand: IdentifyViewFieldMetadataCommand,
protected readonly makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly makeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly makeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
) {
super(
workspaceRepository,
@@ -114,10 +122,16 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.backfillStandardPageLayoutsCommand,
this.identifyFieldMetadataCommand,
this.identifyObjectMetadataCommand,
this.identifyViewMetadataCommand,
this.identifyViewFieldMetadataCommand,
this
.makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this
.makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this
.makeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this
.makeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
];
this.allCommands = {
@@ -0,0 +1,63 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
import { makeViewUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174271-makeViewUniversalIdentifierAndApplicationIdNotNullable.util';
export class MakeViewUniversalIdentifierAndApplicationIdNotNullable1768213174271
implements MigrationInterface
{
name = 'MakeViewUniversalIdentifierAndApplicationIdNotNullable1768213174271';
public async up(queryRunner: QueryRunner): Promise<void> {
const savepointName =
'sp_make_view_universal_identifier_and_application_id_not_nullable';
try {
await queryRunner.query(`SAVEPOINT ${savepointName}`);
await makeViewUniversalIdentifierAndApplicationIdNotNullableQueries(
queryRunner,
);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (e) {
try {
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (rollbackError) {
// eslint-disable-next-line no-console
console.error(
'Failed to rollback to savepoint in MakeViewUniversalIdentifierAndApplicationIdNotNullable1768213174271',
rollbackError,
);
throw rollbackError;
}
// eslint-disable-next-line no-console
console.error(
'Swallowing MakeViewUniversalIdentifierAndApplicationIdNotNullable1768213174271 error',
e,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."view" DROP CONSTRAINT "FK_348e25d584c7e51417f4e097941"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_552aa6908966e980099b3e5ebf"`,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ALTER COLUMN "applicationId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_552aa6908966e980099b3e5ebf" ON "core"."view" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_348e25d584c7e51417f4e097941" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}
@@ -0,0 +1,64 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
import { makeViewFieldUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174272-makeViewFieldUniversalIdentifierAndApplicationIdNotNullable.util';
export class MakeViewFieldUniversalIdentifierAndApplicationIdNotNullable1768213174272
implements MigrationInterface
{
name =
'MakeViewFieldUniversalIdentifierAndApplicationIdNotNullable1768213174272';
public async up(queryRunner: QueryRunner): Promise<void> {
const savepointName =
'sp_make_view_field_universal_identifier_and_application_id_not_nullable';
try {
await queryRunner.query(`SAVEPOINT ${savepointName}`);
await makeViewFieldUniversalIdentifierAndApplicationIdNotNullableQueries(
queryRunner,
);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (e) {
try {
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (rollbackError) {
// eslint-disable-next-line no-console
console.error(
'Failed to rollback to savepoint in MakeViewFieldUniversalIdentifierAndApplicationIdNotNullable1768213174272',
rollbackError,
);
throw rollbackError;
}
// eslint-disable-next-line no-console
console.error(
'Swallowing MakeViewFieldUniversalIdentifierAndApplicationIdNotNullable1768213174272 error',
e,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."viewField" DROP CONSTRAINT "FK_b560ea62a958deff0c6059caa45"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_b86af4ea24cae518dee8eae996"`,
);
await queryRunner.query(
`ALTER TABLE "core"."viewField" ALTER COLUMN "applicationId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."viewField" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_b86af4ea24cae518dee8eae996" ON "core"."viewField" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."viewField" ADD CONSTRAINT "FK_b560ea62a958deff0c6059caa45" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}
@@ -0,0 +1,23 @@
import { type QueryRunner } from 'typeorm';
export const makeViewUniversalIdentifierAndApplicationIdNotNullableQueries =
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.query(
`ALTER TABLE "core"."view" DROP CONSTRAINT "FK_348e25d584c7e51417f4e097941"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_552aa6908966e980099b3e5ebf"`,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ALTER COLUMN "applicationId" SET NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_552aa6908966e980099b3e5ebf" ON "core"."view" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_348e25d584c7e51417f4e097941" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
};
@@ -0,0 +1,23 @@
import { type QueryRunner } from 'typeorm';
export const makeViewFieldUniversalIdentifierAndApplicationIdNotNullableQueries =
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.query(
`ALTER TABLE "core"."viewField" DROP CONSTRAINT "FK_b560ea62a958deff0c6059caa45"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_b86af4ea24cae518dee8eae996"`,
);
await queryRunner.query(
`ALTER TABLE "core"."viewField" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."viewField" ALTER COLUMN "applicationId" SET NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_b86af4ea24cae518dee8eae996" ON "core"."viewField" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."viewField" ADD CONSTRAINT "FK_b560ea62a958deff0c6059caa45" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
};
@@ -14,7 +14,7 @@ import {
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/syncable-entity-required.interface';
@Entity({ name: 'viewField', schema: 'core' })
@Index('IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
@@ -30,7 +30,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
},
)
export class ViewFieldEntity
extends SyncableEntity
extends SyncableEntityRequired
implements Required<ViewFieldEntity>
{
@PrimaryGeneratedColumn('uuid')
@@ -27,7 +27,7 @@ import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
import { ViewVisibility } from 'src/engine/metadata-modules/view/enums/view-visibility.enum';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/syncable-entity-required.interface';
// We could refactor this type to be dynamic to view type
@Entity({ name: 'view', schema: 'core' })
@@ -40,7 +40,10 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
'CHK_VIEW_CALENDAR_INTEGRITY',
`("type" != 'CALENDAR' OR ("calendarLayout" IS NOT NULL AND "calendarFieldMetadataId" IS NOT NULL))`,
)
export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
export class ViewEntity
extends SyncableEntityRequired
implements Required<ViewEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;