From 8d33264a7d04e28df0e3a36e653bcfa46806c895 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:57:30 +0200 Subject: [PATCH] Migrate fields of deprecated type LINK to type LINKS (#6332) Closes #5909. Adding a command to migrate fields of type Link to fields of type Links, including their data. --- .../commands/database-command.module.ts | 2 + ...23-migrate-link-fields-to-links.command.ts | 341 ++++++++++++++++++ .../0-23/0-23-upgrade-version.command.ts | 33 ++ .../0-23/0-23-upgrade-version.module.ts | 30 ++ .../field-metadata/field-metadata.module.ts | 4 + .../field-metadata/field-metadata.service.ts | 40 +- .../twenty-orm/twenty-orm-core.module.ts | 3 + .../twenty-orm/twenty-orm-global.manager.ts | 114 ++++++ .../company.workspace-entity.ts | 4 +- .../src/modules/modules.module.ts | 3 +- .../person.workspace-entity.ts | 4 +- .../src/modules/view/services/view.service.ts | 99 +++++ .../src/modules/view/view.module.ts | 10 + 13 files changed, 668 insertions(+), 19 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts create mode 100644 packages/twenty-server/src/modules/view/services/view.service.ts create mode 100644 packages/twenty-server/src/modules/view/view.module.ts diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index eaaaa8624..f3a1e61ea 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -9,6 +9,7 @@ import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-wo import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; import { UpdateMessageChannelVisibilityEnumCommand } from 'src/database/commands/upgrade-version/0-20/0-20-update-message-channel-visibility-enum.command'; import { UpgradeTo0_22CommandModule } from 'src/database/commands/upgrade-version/0-22/0-22-upgrade-version.module'; +import { UpgradeTo0_23CommandModule } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module'; import { WorkspaceAddTotalCountCommand } from 'src/database/commands/workspace-add-total-count.command'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; @@ -47,6 +48,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp WorkspaceCacheVersionModule, // Upgrades UpgradeTo0_22CommandModule, + UpgradeTo0_23CommandModule, ], providers: [ DataSeedWorkspaceCommand, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts new file mode 100644 index 000000000..f70bb5fcc --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command.ts @@ -0,0 +1,341 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { QueryRunner, Repository } from 'typeorm'; + +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; +import { FieldMetadataDefaultValueLink } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service'; +import { ViewService } from 'src/modules/view/services/view.service'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; + +interface MigrateLinkFieldsToLinksCommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'migrate-0.23:migrate-link-fields-to-links', + description: 'Migrating fields of deprecated type LINK to type LINKS', +}) +export class MigrateLinkFieldsToLinksCommand extends CommandRunner { + private readonly logger = new Logger(MigrateLinkFieldsToLinksCommand.name); + constructor( + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly fieldMetadataService: FieldMetadataService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly workspaceStatusService: WorkspaceStatusService, + private readonly viewService: ViewService, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + _passedParam: string[], + options: MigrateLinkFieldsToLinksCommandOptions, + ): Promise { + this.logger.log( + 'Running command to migrate link type fields to links type', + ); + let workspaceIds: string[] = []; + + if (options.workspaceId) { + workspaceIds = [options.workspaceId]; + } else { + const activeWorkspaceIds = + await this.workspaceStatusService.getActiveWorkspaceIds(); + + workspaceIds = activeWorkspaceIds; + } + + if (!workspaceIds.length) { + this.logger.log(chalk.yellow('No workspace found')); + + return; + } else { + this.logger.log( + chalk.green(`Running command on ${workspaceIds.length} workspaces`), + ); + } + + for (const workspaceId of workspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + try { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (!workspaceDataSource) { + throw new Error( + `Could not connect to dataSource for workspace ${workspaceId}`, + ); + } + + const fieldsWithLinkType = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.LINK, + }, + }); + + for (const fieldWithLinkType of fieldsWithLinkType) { + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { id: fieldWithLinkType.objectMetadataId }, + }); + + if (!objectMetadata) { + throw new Error( + `Could not find objectMetadata for field ${fieldWithLinkType.name}`, + ); + } + + this.logger.log( + `Attempting to migrate field ${fieldWithLinkType.name} on ${objectMetadata.nameSingular}.`, + ); + const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); + + await workspaceQueryRunner.connect(); + + const fieldName = fieldWithLinkType.name; + const { id: _id, ...fieldWithLinkTypeWithoutId } = fieldWithLinkType; + + const linkDefaultValue = + fieldWithLinkTypeWithoutId.defaultValue as FieldMetadataDefaultValueLink; + + const defaultValueForLinksField = { + primaryLinkUrl: linkDefaultValue.url, + primaryLinkLabel: linkDefaultValue.label, + secondaryLinks: null, + }; + + try { + const tmpNewLinksField = await this.fieldMetadataService.createOne({ + ...fieldWithLinkTypeWithoutId, + type: FieldMetadataType.LINKS, + defaultValue: defaultValueForLinksField, + name: `${fieldName}Tmp`, + } satisfies CreateFieldInput); + + const tableName = computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ); + + // Migrate data from linkLabel to primaryLinkLabel + await this.migrateDataWithinTable({ + sourceColumnName: `${fieldWithLinkType.name}Label`, + targetColumnName: `${tmpNewLinksField.name}PrimaryLinkLabel`, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }); + + // Migrate data from linkUrl to primaryLinkUrl + await this.migrateDataWithinTable({ + sourceColumnName: `${fieldWithLinkType.name}Url`, + targetColumnName: `${tmpNewLinksField.name}PrimaryLinkUrl`, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }); + + // Duplicate link field's views behaviour for new links field + await this.viewService.removeFieldFromViews({ + workspaceId: workspaceId, + fieldId: tmpNewLinksField.id, + }); + + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + ViewFieldWorkspaceEntity, + ); + const viewFieldsWithDeprecatedField = + await viewFieldRepository.find({ + where: { + fieldMetadataId: fieldWithLinkType.id, + isVisible: true, + }, + }); + + await this.viewService.addFieldToViews({ + workspaceId: workspaceId, + fieldId: tmpNewLinksField.id, + viewsIds: viewFieldsWithDeprecatedField + .filter((viewField) => viewField.viewId !== null) + .map((viewField) => viewField.viewId as string), + positions: viewFieldsWithDeprecatedField.reduce( + (acc, viewField) => { + if (!viewField.viewId) { + return acc; + } + acc[viewField.viewId] = viewField.position; + + return acc; + }, + [], + ), + }); + + // Delete link field + await this.fieldMetadataService.deleteOneField( + { id: fieldWithLinkType.id }, + workspaceId, + ); + + // Rename temporary links field + await this.fieldMetadataService.updateOne(tmpNewLinksField.id, { + id: tmpNewLinksField.id, + workspaceId: tmpNewLinksField.workspaceId, + name: `${fieldName}`, + }); + + this.logger.log( + `Migration of ${fieldWithLinkType.name} on ${objectMetadata.nameSingular} done!`, + ); + } catch (error) { + this.logger.log( + `Failed to migrate field ${fieldWithLinkType.name} on ${objectMetadata.nameSingular}, rolling back.`, + ); + + // Re-create initial field if it was deleted + const initialField = + await this.fieldMetadataService.findOneWithinWorkspace( + workspaceId, + { + where: { + name: `${fieldWithLinkType.name}`, + objectMetadataId: fieldWithLinkType.objectMetadataId, + }, + }, + ); + + const tmpNewLinksField = + await this.fieldMetadataService.findOneWithinWorkspace( + workspaceId, + { + where: { + name: `${fieldWithLinkType.name}Tmp`, + objectMetadataId: fieldWithLinkType.objectMetadataId, + }, + }, + ); + + if (!initialField) { + this.logger.log( + `Re-creating initial link field ${fieldWithLinkType.name} but of type links`, // Cannot create link fields anymore + ); + const restoredField = await this.fieldMetadataService.createOne({ + ...fieldWithLinkType, + defaultValue: defaultValueForLinksField, + type: FieldMetadataType.LINKS, + }); + const tableName = computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ); + + if (tmpNewLinksField) { + this.logger.log( + `Restoring data in field ${fieldWithLinkType.name}`, + ); + await this.migrateDataWithinTable({ + sourceColumnName: `${tmpNewLinksField.name}PrimaryLinkLabel`, + targetColumnName: `${restoredField.name}PrimaryLinkLabel`, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }); + + await this.migrateDataWithinTable({ + sourceColumnName: `${tmpNewLinksField.name}PrimaryLinkUrl`, + targetColumnName: `${restoredField.name}PrimaryLinkUrl`, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }); + } else { + this.logger.log( + `Failed to restore data in link field ${fieldWithLinkType.name}`, + ); + } + } + + if (tmpNewLinksField) { + await this.fieldMetadataService.deleteOneField( + { id: tmpNewLinksField.id }, + workspaceId, + ); + } + } finally { + await workspaceQueryRunner.release(); + } + } + } catch (error) { + this.logger.log( + chalk.red( + `Running command on workspace ${workspaceId} failed with error: ${error}`, + ), + ); + continue; + } + + this.logger.log(chalk.green(`Command completed!`)); + } + } + + private async migrateDataWithinTable({ + sourceColumnName, + targetColumnName, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }: { + sourceColumnName: string; + targetColumnName: string; + tableName: string; + workspaceQueryRunner: QueryRunner; + dataSourceMetadata: DataSourceEntity; + }) { + await workspaceQueryRunner.query( + `UPDATE "${dataSourceMetadata.schema}"."${tableName}" SET "${targetColumnName}" = "${sourceColumnName}"`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts new file mode 100644 index 000000000..29b3ee11a --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts @@ -0,0 +1,33 @@ +import { Command, CommandRunner, Option } from 'nest-commander'; + +import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; + +interface Options { + workspaceId?: string; +} + +@Command({ + name: 'upgrade-0.23', + description: 'Upgrade to 0.23', +}) +export class UpgradeTo0_23Command extends CommandRunner { + constructor( + private readonly migrateLinkFieldsToLinks: MigrateLinkFieldsToLinksCommand, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run(_passedParam: string[], options: Options): Promise { + await this.migrateLinkFieldsToLinks.run(_passedParam, options); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts new file mode 100644 index 000000000..5d5aa86ad --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; +import { UpgradeTo0_23Command } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command'; +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { Workspace } 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; +import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module'; +import { ViewModule } from 'src/modules/view/view.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + WorkspaceCacheVersionModule, + FieldMetadataModule, + DataSourceModule, + WorkspaceStatusModule, + TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), + TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + TypeORMModule, + ViewModule, + ], + providers: [MigrateLinkFieldsToLinksCommand, UpgradeTo0_23Command], +}) +export class UpgradeTo0_23CommandModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 321bbc9d9..b26e5ffa4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -18,7 +18,9 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; +import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module'; import { FieldMetadataEntity } from './field-metadata.entity'; import { FieldMetadataService } from './field-metadata.service'; @@ -32,6 +34,8 @@ import { UpdateFieldInput } from './dtos/update-field.input'; imports: [ NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), WorkspaceMigrationModule, + WorkspaceStatusModule, + TwentyORMModule, WorkspaceMigrationRunnerModule, WorkspaceCacheVersionModule, ObjectMetadataModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 0154f9dcd..9dfe1415c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; +import isEmpty from 'lodash.isempty'; import { DataSource, FindOneOptions, Repository } from 'typeorm'; import { v4 as uuidV4 } from 'uuid'; @@ -51,6 +52,7 @@ import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { FieldMetadataEntity, @@ -224,28 +226,38 @@ export class FieldMetadataService extends TypeOrmQueryService viewField.position) - .reduce((acc, position) => { - if (position > acc) { - return position; - } + const createdFieldIsAlreadyInView = existingViewFields.some( + (existingViewField) => + existingViewField.fieldMetadataId === createdFieldMetadata.id, + ); - return acc; - }, -1); + if (!createdFieldIsAlreadyInView) { + const lastPosition = existingViewFields + .map((viewField) => viewField.position) + .reduce((acc, position) => { + if (position > acc) { + return position; + } - await workspaceQueryRunner?.query( - `INSERT INTO ${dataSourceMetadata.schema}."viewField" + return acc; + }, -1); + + await workspaceQueryRunner?.query( + `INSERT INTO ${dataSourceMetadata.schema}."viewField" ("fieldMetadataId", "position", "isVisible", "size", "viewId") VALUES ('${createdFieldMetadata.id}', '${lastPosition + 1}', true, 180, '${ view[0].id }')`, - ); + ); + } + } + await workspaceQueryRunner.commitTransaction(); } catch (error) { await workspaceQueryRunner.rollbackTransaction(); diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts index f9f959d1e..dc6540a84 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts @@ -25,6 +25,7 @@ import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-sche import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { @@ -48,12 +49,14 @@ export const workspaceDataSourceCacheInstance = providers: [ ...entitySchemaFactories, TwentyORMManager, + TwentyORMGlobalManager, LoadServiceWithWorkspaceContext, ], exports: [ EntitySchemaFactory, TwentyORMManager, LoadServiceWithWorkspaceContext, + TwentyORMGlobalManager, ], }) export class TwentyORMCoreModule diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts new file mode 100644 index 000000000..1fc1f0b04 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts @@ -0,0 +1,114 @@ +import { Injectable, Type } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { ObjectLiteral, Repository } from 'typeorm'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; +import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; +import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { workspaceDataSourceCacheInstance } from 'src/engine/twenty-orm/twenty-orm-core.module'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; + +@Injectable() +export class TwentyORMGlobalManager { + constructor( + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory, + private readonly entitySchemaFactory: EntitySchemaFactory, + ) {} + + async getRepositoryForWorkspace( + workspaceId: string, + entityClass: Type, + ): Promise>; + + async getRepositoryForWorkspace( + workspaceId: string, + objectMetadataName: string, + ): Promise>; + + async getRepositoryForWorkspace( + workspaceId: string, + entityClassOrobjectMetadataName: Type | string, + ): Promise< + WorkspaceRepository | WorkspaceRepository + > { + let objectMetadataName: string; + + if (typeof entityClassOrobjectMetadataName === 'string') { + objectMetadataName = entityClassOrobjectMetadataName; + } else { + objectMetadataName = convertClassNameToObjectMetadataName( + entityClassOrobjectMetadataName.name, + ); + } + + return this.buildRepositoryForWorkspace(workspaceId, objectMetadataName); + } + + async buildDatasourceForWorkspace(workspaceId: string) { + const cacheVersion = + await this.workspaceCacheVersionService.getVersion(workspaceId); + + let objectMetadataCollection = + await this.workspaceCacheStorageService.getObjectMetadataCollection( + workspaceId, + ); + + if (!objectMetadataCollection) { + objectMetadataCollection = await this.objectMetadataRepository.find({ + where: { workspaceId }, + relations: [ + 'fields.object', + 'fields', + 'fields.fromRelationMetadata', + 'fields.toRelationMetadata', + 'fields.fromRelationMetadata.toObjectMetadata', + ], + }); + + await this.workspaceCacheStorageService.setObjectMetadataCollection( + workspaceId, + objectMetadataCollection, + ); + } + + const entities = await Promise.all( + objectMetadataCollection.map((objectMetadata) => + this.entitySchemaFactory.create(workspaceId, objectMetadata), + ), + ); + + return await workspaceDataSourceCacheInstance.execute( + `${workspaceId}-${cacheVersion}`, + async () => { + const workspaceDataSource = + await this.workspaceDataSourceFactory.create(entities, workspaceId); + + return workspaceDataSource; + }, + (dataSource) => dataSource.destroy(), + ); + } + + async buildRepositoryForWorkspace( + workspaceId: string, + objectMetadataName: string, + ) { + const workspaceDataSource = + await this.buildDatasourceForWorkspace(workspaceId); + + if (!workspaceDataSource) { + throw new Error('Workspace data source not found'); + } + + return workspaceDataSource.getRepository(objectMetadataName); + } +} diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index a6ec959b9..fa8034b41 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -68,7 +68,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.linkedinLink, - type: FieldMetadataType.LINK, + type: FieldMetadataType.LINKS, label: 'Linkedin', description: 'The company Linkedin account', icon: 'IconBrandLinkedin', @@ -78,7 +78,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.xLink, - type: FieldMetadataType.LINK, + type: FieldMetadataType.LINKS, label: 'X', description: 'The company Twitter/X account', icon: 'IconBrandX', diff --git a/packages/twenty-server/src/modules/modules.module.ts b/packages/twenty-server/src/modules/modules.module.ts index 2d966e073..d43e94baf 100644 --- a/packages/twenty-server/src/modules/modules.module.ts +++ b/packages/twenty-server/src/modules/modules.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { CalendarModule } from 'src/modules/calendar/calendar.module'; import { MessagingModule } from 'src/modules/messaging/messaging.module'; +import { ViewModule } from 'src/modules/view/view.module'; @Module({ - imports: [MessagingModule, CalendarModule], + imports: [MessagingModule, CalendarModule, ViewModule], providers: [], exports: [], }) diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index 6d2923775..ce46bb2c8 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -56,7 +56,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.linkedinLink, - type: FieldMetadataType.LINK, + type: FieldMetadataType.LINKS, label: 'Linkedin', description: 'Contact’s Linkedin account', icon: 'IconBrandLinkedin', @@ -66,7 +66,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.xLink, - type: FieldMetadataType.LINK, + type: FieldMetadataType.LINKS, label: 'X', description: 'Contact’s X/Twitter account', icon: 'IconBrandX', diff --git a/packages/twenty-server/src/modules/view/services/view.service.ts b/packages/twenty-server/src/modules/view/services/view.service.ts new file mode 100644 index 000000000..db6b02bb6 --- /dev/null +++ b/packages/twenty-server/src/modules/view/services/view.service.ts @@ -0,0 +1,99 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; +import isEmpty from 'lodash.isempty'; + +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; + +@Injectable() +export class ViewService { + private readonly logger = new Logger(ViewService.name); + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async addFieldToViews({ + workspaceId, + fieldId, + viewsIds, + positions, + }: { + workspaceId: string; + fieldId: string; + viewsIds: string[]; + positions?: { + [key: string]: number; + }[]; + }) { + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + ViewFieldWorkspaceEntity, + ); + + for (const viewId of viewsIds) { + const position = positions?.[viewId]; + const newFieldInThisView = await viewFieldRepository.findBy({ + fieldMetadataId: fieldId, + viewId: viewId as string, + isVisible: true, + }); + + if (!isEmpty(newFieldInThisView)) { + continue; + } + + this.logger.log( + `Adding new field ${fieldId} to view ${viewId} for workspace ${workspaceId}...`, + ); + const newViewField = viewFieldRepository.create({ + viewId: viewId, + fieldMetadataId: fieldId, + isVisible: true, + ...(isDefined(position) && { position: position }), + }); + + await viewFieldRepository.save(newViewField); + this.logger.log( + `New field successfully added to view ${viewId} for workspace ${workspaceId}`, + ); + } + } + + async removeFieldFromViews({ + workspaceId, + fieldId, + }: { + workspaceId: string; + fieldId: string; + }) { + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + ViewFieldWorkspaceEntity, + ); + const viewsWithField = await viewFieldRepository.find({ + where: { + fieldMetadataId: fieldId, + isVisible: true, + }, + }); + + for (const viewWithField of viewsWithField) { + const viewId = viewWithField.viewId; + + this.logger.log( + `Removing field ${fieldId} from view ${viewId} for workspace ${workspaceId}...`, + ); + await viewFieldRepository.delete({ + viewId: viewWithField.viewId as string, + fieldMetadataId: fieldId, + }); + + this.logger.log( + `Field ${fieldId} successfully removed from view ${viewId} for workspace ${workspaceId}`, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/view/view.module.ts b/packages/twenty-server/src/modules/view/view.module.ts new file mode 100644 index 000000000..5ae3dbd98 --- /dev/null +++ b/packages/twenty-server/src/modules/view/view.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { ViewService } from 'src/modules/view/services/view.service'; + +@Module({ + imports: [], + providers: [ViewService], + exports: [ViewService], +}) +export class ViewModule {}