From 859e718cf2e023ffd3e33e3cc77db278fbf3955e Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:56:52 +0100 Subject: [PATCH] Identify view group (#17220) # Introduction Related to 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 standard ids ## Test tested prod extract --- ...16-identify-view-group-metadata.command.ts | 340 ++++++++++++++++++ ...ation-id-not-nullable-migration.command.ts | 71 ++++ .../1-16-upgrade-version-command.module.ts | 8 + .../upgrade.command.ts | 7 + ...alIdentifierAndApplicationIdNotNullable.ts | 64 ++++ ...ntifierAndApplicationIdNotNullable.util.ts | 23 ++ .../view-group/entities/view-group.entity.ts | 4 +- 7 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command.ts new file mode 100644 index 0000000000..4e63a98785 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command.ts @@ -0,0 +1,340 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { IsNull, Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; +import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; +import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant'; + +const VIEW_GROUP_TO_FIELD_VALUE_MAPPING: Record< + string, + Record> +> = { + opportunity: { + byStage: { + new: 'NEW', + screening: 'SCREENING', + meeting: 'MEETING', + proposal: 'PROPOSAL', + customer: 'CUSTOMER', + }, + }, + task: { + assignedToMe: { + todo: 'TODO', + inProgress: 'IN_PROGRESS', + done: 'DONE', + empty: '', + }, + byStatus: { + todo: 'TODO', + inProgress: 'IN_PROGRESS', + done: 'DONE', + }, + }, +}; + +type StandardViewGroupUpdate = { + flatViewGroup: FlatViewGroup; + universalIdentifier: string; + objectNameSingular: string; + viewName: string; + viewGroupName: string; +}; + +@Command({ + name: 'upgrade:1-16:identify-view-group-metadata', + description: 'Identify standard view group metadata', +}) +export class IdentifyViewGroupMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + @InjectRepository(ViewGroupEntity) + private readonly viewGroupRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + protected readonly applicationService: ApplicationService, + protected readonly workspaceCacheService: WorkspaceCacheService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Running identify standard view group metadata for workspace ${workspaceId}`, + ); + + const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const { flatObjectMetadataMaps, flatViewMaps, flatViewGroupMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatObjectMetadataMaps', + 'flatViewMaps', + 'flatViewGroupMaps', + ], + }, + ); + + await this.identifyStandardViewGroups({ + flatObjectMetadataMaps, + flatViewMaps, + flatViewGroupMaps, + twentyStandardApplicationId: twentyStandardFlatApplication.id, + dryRun: options.dryRun ?? false, + }); + + await this.identifyCustomViewGroups({ + workspaceId, + flatObjectMetadataMaps, + flatViewMaps, + workspaceCustomApplicationId: workspaceCustomFlatApplication.id, + dryRun: options.dryRun ?? false, + }); + + const relatedMetadataNames = getMetadataRelatedMetadataNames('viewGroup'); + const relatedCacheKeysToInvalidate = relatedMetadataNames.map( + getMetadataFlatEntityMapsKey, + ); + + this.logger.log( + `Invalidating caches: ${relatedCacheKeysToInvalidate.join(' ')}`, + ); + if (!options.dryRun) { + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatViewGroupMaps', + ...relatedCacheKeysToInvalidate, + ]); + } + } + + private async identifyStandardViewGroups({ + flatObjectMetadataMaps, + flatViewMaps, + flatViewGroupMaps, + twentyStandardApplicationId, + dryRun, + }: { + flatObjectMetadataMaps: FlatEntityMaps; + flatViewMaps: FlatEntityMaps; + flatViewGroupMaps: FlatEntityMaps; + twentyStandardApplicationId: string; + dryRun: boolean; + }): Promise { + const standardViewGroupUpdates: StandardViewGroupUpdate[] = []; + + for (const [objectNameSingular, objectConfig] of Object.entries( + STANDARD_OBJECTS, + )) { + const objectViews = + 'views' in objectConfig + ? (objectConfig.views as Record< + string, + | { + universalIdentifier: string; + viewGroups?: Record< + string, + { universalIdentifier: string } | undefined + >; + } + | undefined + >) + : null; + + if (!isDefined(objectViews)) { + continue; + } + + const flatObjectMetadata = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatObjectMetadataMaps, + universalIdentifier: objectConfig.universalIdentifier, + }); + + if (!isDefined(flatObjectMetadata)) { + this.logger.error( + `Standard object "${objectNameSingular}" not found in workspace, this needs investigation, skipping`, + ); + continue; + } + + const objectViewGroupMapping = + VIEW_GROUP_TO_FIELD_VALUE_MAPPING[objectNameSingular]; + + for (const [viewName, viewConfig] of Object.entries(objectViews)) { + if (!isDefined(viewConfig) || !isDefined(viewConfig.viewGroups)) { + continue; + } + + const flatView = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatViewMaps, + universalIdentifier: viewConfig.universalIdentifier, + }); + + if (!isDefined(flatView)) { + this.logger.warn( + `Standard view "${viewName}" not found for object "${flatObjectMetadata.nameSingular}", skipping view groups`, + ); + continue; + } + + const relatedFlatViewGroups = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: flatView.viewGroupIds, + flatEntityMaps: flatViewGroupMaps, + }); + + const viewGroupMapping = objectViewGroupMapping?.[viewName]; + + for (const [viewGroupName, viewGroupConfig] of Object.entries( + viewConfig.viewGroups, + )) { + if (!isDefined(viewGroupConfig)) { + continue; + } + + const fieldValue = viewGroupMapping?.[viewGroupName]; + + if (!isDefined(fieldValue) && fieldValue !== '') { + this.logger.warn( + `Field value mapping for view group "${viewGroupName}" not found for view "${viewName}" of object "${flatObjectMetadata.nameSingular}", skipping view group`, + ); + continue; + } + + const matchingFlatViewGroup = relatedFlatViewGroups.find( + (viewGroup) => viewGroup.fieldValue === fieldValue, + ); + + if (!isDefined(matchingFlatViewGroup)) { + this.logger.warn( + `Standard view group "${viewGroupName}" with fieldValue="${fieldValue}" not found for view "${viewName}" of object "${flatObjectMetadata.nameSingular}", skipping`, + ); + continue; + } + + if (isDefined(matchingFlatViewGroup.applicationId)) { + continue; + } + + standardViewGroupUpdates.push({ + flatViewGroup: matchingFlatViewGroup, + universalIdentifier: viewGroupConfig.universalIdentifier, + objectNameSingular: flatObjectMetadata.nameSingular, + viewName: flatView.name, + viewGroupName, + }); + } + } + } + + const standardUpdates = standardViewGroupUpdates.map( + ({ flatViewGroup, universalIdentifier }) => ({ + id: flatViewGroup.id, + universalIdentifier, + applicationId: twentyStandardApplicationId, + }), + ); + + this.logger.log( + `Found ${standardUpdates.length} standard view group(s) to update`, + ); + + for (const { + flatViewGroup, + universalIdentifier, + objectNameSingular, + viewName, + viewGroupName, + } of standardViewGroupUpdates) { + this.logger.log( + ` - Standard view group "${viewGroupName}" on view "${viewName}" of object "${objectNameSingular}" (id=${flatViewGroup.id}) -> universalIdentifier=${universalIdentifier}`, + ); + } + + if (!dryRun) { + await this.viewGroupRepository.save(standardUpdates); + } + } + + private async identifyCustomViewGroups({ + workspaceId, + flatObjectMetadataMaps, + flatViewMaps, + workspaceCustomApplicationId, + dryRun, + }: { + workspaceId: string; + flatObjectMetadataMaps: FlatEntityMaps; + flatViewMaps: FlatEntityMaps; + workspaceCustomApplicationId: string; + dryRun: boolean; + }): Promise { + const remainingCustomViewGroups = await this.viewGroupRepository.find({ + select: { + id: true, + universalIdentifier: true, + applicationId: true, + viewId: true, + fieldValue: true, + }, + where: { + workspaceId, + applicationId: IsNull(), + }, + withDeleted: true, + }); + + const customUpdates = remainingCustomViewGroups.map((viewGroupEntity) => ({ + id: viewGroupEntity.id, + universalIdentifier: viewGroupEntity.universalIdentifier ?? v4(), + applicationId: workspaceCustomApplicationId, + })); + + this.logger.log( + `Found ${customUpdates.length} custom view group(s) to update for workspace ${workspaceId}`, + ); + + for (const viewGroupEntity of remainingCustomViewGroups) { + const flatView = flatViewMaps.byId[viewGroupEntity.viewId]; + const flatObjectMetadata = isDefined(flatView) + ? flatObjectMetadataMaps.byId[flatView.objectMetadataId] + : undefined; + + this.logger.log( + ` - Custom view group with fieldValue="${viewGroupEntity.fieldValue}" on view "${flatView?.name ?? 'unknown'}" of object "${flatObjectMetadata?.nameSingular ?? 'unknown'}" (id=${viewGroupEntity.id})`, + ); + } + + if (!dryRun) { + await this.viewGroupRepository.save(customUpdates); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command.ts new file mode 100644 index 0000000000..b171ecc71a --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command.ts @@ -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 { makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +@Command({ + name: 'upgrade:1-16:make-view-group-universal-identifier-and-application-id-not-nullable-migration', + description: + 'Make universalIdentifier and applicationId columns NOT NULL on viewGroup table', +}) +export class MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + private hasRunOnce = false; + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + @InjectDataSource() + private readonly coreDataSource: DataSource, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + options, + }: RunOnWorkspaceArgs): Promise { + if (this.hasRunOnce) { + this.logger.warn( + 'Skipping has already been run once MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries( + queryRunner, + ); + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Rolling back MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, + ); + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts index f79a94aa8b..614416d787 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts @@ -8,12 +8,14 @@ import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-vers 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 { IdentifyViewFilterMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-filter-metadata.command'; +import { IdentifyViewGroupMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command'; import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.command'; import { MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-agent-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command'; import { 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 { MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-filter-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-universal-identifier-and-application-id-not-nullable-migration.command'; import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; @@ -26,6 +28,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata 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 { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; +import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @@ -42,6 +45,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace ViewEntity, ViewFieldEntity, ViewFilterEntity, + ViewGroupEntity, ]), DataSourceModule, WorkspaceCacheModule, @@ -63,11 +67,13 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace IdentifyViewFieldMetadataCommand, IdentifyViewFilterMetadataCommand, MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + IdentifyViewGroupMetadataCommand, MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ], exports: [ UpdateTaskOnDeleteActionCommand, @@ -80,11 +86,13 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace IdentifyViewFieldMetadataCommand, IdentifyViewFilterMetadataCommand, MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + IdentifyViewGroupMetadataCommand, MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ], }) export class V1_16_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 8c2d973e49..bbfaccc25e 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -29,12 +29,14 @@ import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-vers 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 { IdentifyViewFilterMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-filter-metadata.command'; +import { IdentifyViewGroupMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command'; import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.command'; import { MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-agent-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command'; import { 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 { MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-filter-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-group-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-universal-identifier-and-application-id-not-nullable-migration.command'; import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -86,11 +88,13 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly identifyViewFieldMetadataCommand: IdentifyViewFieldMetadataCommand, protected readonly identifyViewFilterMetadataCommand: IdentifyViewFilterMetadataCommand, protected readonly makeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + protected readonly identifyViewGroupMetadataCommand: IdentifyViewGroupMetadataCommand, protected readonly makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly makeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly makeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly makeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly makeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + protected readonly makeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ) { super( workspaceRepository, @@ -134,6 +138,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.identifyViewMetadataCommand, this.identifyViewFieldMetadataCommand, this.identifyViewFilterMetadataCommand, + this.identifyViewGroupMetadataCommand, this .makeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, this @@ -146,6 +151,8 @@ export class UpgradeCommand extends UpgradeCommandRunner { .makeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, this .makeViewFilterUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + this + .makeViewGroupUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ]; this.allCommands = { diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.ts new file mode 100644 index 0000000000..aa6eeaf017 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.ts @@ -0,0 +1,64 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +import { makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util'; + +export class MakeViewGroupUniversalIdentifierAndApplicationIdNotNullable1768213174274 + implements MigrationInterface +{ + name = + 'MakeViewGroupUniversalIdentifierAndApplicationIdNotNullable1768213174274'; + + public async up(queryRunner: QueryRunner): Promise { + const savepointName = + 'sp_make_view_group_universal_identifier_and_application_id_not_nullable'; + + try { + await queryRunner.query(`SAVEPOINT ${savepointName}`); + + await makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries( + 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 MakeViewGroupUniversalIdentifierAndApplicationIdNotNullable1768213174274', + rollbackError, + ); + throw rollbackError; + } + + // eslint-disable-next-line no-console + console.error( + 'Swallowing MakeViewGroupUniversalIdentifierAndApplicationIdNotNullable1768213174274 error', + e, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" DROP CONSTRAINT "FK_5aff384532c78fa8a42ceeae282"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_a44e3b03f0eca32d0504d5ef73"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ALTER COLUMN "applicationId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ALTER COLUMN "universalIdentifier" DROP NOT NULL`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_a44e3b03f0eca32d0504d5ef73" ON "core"."viewGroup" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ADD CONSTRAINT "FK_5aff384532c78fa8a42ceeae282" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util.ts b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util.ts new file mode 100644 index 0000000000..9e4f9b6d90 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1768213174274-makeViewGroupUniversalIdentifierAndApplicationIdNotNullable.util.ts @@ -0,0 +1,23 @@ +import { type QueryRunner } from 'typeorm'; + +export const makeViewGroupUniversalIdentifierAndApplicationIdNotNullableQueries = + async (queryRunner: QueryRunner): Promise => { + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" DROP CONSTRAINT "FK_5aff384532c78fa8a42ceeae282"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_a44e3b03f0eca32d0504d5ef73"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ALTER COLUMN "universalIdentifier" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ALTER COLUMN "applicationId" SET NOT NULL`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_a44e3b03f0eca32d0504d5ef73" ON "core"."viewGroup" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewGroup" ADD CONSTRAINT "FK_5aff384532c78fa8a42ceeae282" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + }; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/entities/view-group.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/entities/view-group.entity.ts index 824b73b3e9..5a7f51300a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/entities/view-group.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/entities/view-group.entity.ts @@ -12,13 +12,13 @@ import { } from 'typeorm'; 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: 'viewGroup', schema: 'core' }) @Index('IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) @Index('IDX_VIEW_GROUP_VIEW_ID', ['viewId']) export class ViewGroupEntity - extends SyncableEntity + extends SyncableEntityRequired implements Required { @PrimaryGeneratedColumn('uuid')