diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 81293fcf23..168aae6f81 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -259,6 +259,7 @@ export enum AllMetadataName { pageLayout = 'pageLayout', pageLayoutTab = 'pageLayoutTab', pageLayoutWidget = 'pageLayoutWidget', + permissionFlag = 'permissionFlag', role = 'role', roleTarget = 'roleTarget', rowLevelPermissionPredicate = 'rowLevelPermissionPredicate', diff --git a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts index c4e2823a81..fdfc04edd0 100644 --- a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts +++ b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts @@ -30,6 +30,7 @@ export const useMetadataErrorHandler = () => { viewFilter: t`view filter`, index: t`index`, logicFunction: t`logic function`, + permissionFlag: t`permission flag`, role: t`role`, roleTarget: t`role target`, agent: t`agent`, diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql index 3f2fead812..eb63cc733b 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql @@ -2239,6 +2239,7 @@ enum AllMetadataName { pageLayoutTab commandMenuItem navigationMenuItem + permissionFlag frontComponent webhook } diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts index 212c4b415e..f2568c8217 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts @@ -1892,7 +1892,7 @@ export interface CollectionHash { __typename: 'CollectionHash' } -export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'frontComponent' | 'webhook' +export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'frontComponent' | 'webhook' export interface MinimalObjectMetadata { id: Scalars['UUID'] @@ -8788,6 +8788,7 @@ export const enumAllMetadataName = { pageLayoutTab: 'pageLayoutTab' as const, commandMenuItem: 'commandMenuItem' as const, navigationMenuItem: 'navigationMenuItem' as const, + permissionFlag: 'permissionFlag' as const, frontComponent: 'frontComponent' as const, webhook: 'webhook' as const } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts new file mode 100644 index 0000000000..9f526d40ad --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command.ts @@ -0,0 +1,112 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, IsNull, type Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { isDefined } from 'twenty-shared/utils'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +@Command({ + name: 'upgrade:1-20:identify-permission-flag-metadata', + description: + 'Identify permission flag metadata (backfill universalIdentifier and applicationId)', +}) +export class IdentifyPermissionFlagMetadataCommand 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.log( + 'Skipping has already been run once IdentifyPermissionFlagMetadataCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const repository = + queryRunner.manager.getRepository(PermissionFlagEntity); + const withNullApplicationId = await repository.find({ + where: { + applicationId: IsNull(), + }, + relations: ['role'], + }); + + const toUpdate = withNullApplicationId.filter((permissionFlag) => + isDefined(permissionFlag.role?.applicationId), + ); + const toRemove = withNullApplicationId.filter( + (permissionFlag) => !isDefined(permissionFlag.role?.applicationId), + ); + + for (const permissionFlag of toUpdate) { + const flag = permissionFlag; + flag.applicationId = permissionFlag.role.applicationId; + flag.universalIdentifier = flag.universalIdentifier ?? v4(); + } + + if (toUpdate.length > 0) { + await repository.save(toUpdate); + } + if (toRemove.length > 0) { + await repository.remove(toRemove); + } + + const withNullUniversalIdentifier = await repository.find({ + where: { + universalIdentifier: IsNull(), + }, + }); + + for (const permissionFlag of withNullUniversalIdentifier) { + permissionFlag.universalIdentifier = v4(); + } + + if (withNullUniversalIdentifier.length > 0) { + await repository.save(withNullUniversalIdentifier); + } + + await queryRunner.commitTransaction(); + this.logger.log('Successfully run IdentifyPermissionFlagMetadataCommand'); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Rolling back IdentifyPermissionFlagMetadataCommand: ${error.message}`, + ); + throw error; + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command.ts new file mode 100644 index 0000000000..77bd5e2979 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command.ts @@ -0,0 +1,72 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, type Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773232418467-make-permission-flag-universal-identifier-and-application-id-not-null.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-20:make-permission-flag-universal-identifier-and-application-id-not-nullable-migration', + description: + 'Set NOT NULL on permissionFlag universalIdentifier and applicationId, add unique index and FK (run identify-permission-flag-metadata first)', +}) +export class MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand 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.log( + 'Skipping has already been run once MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries( + queryRunner, + ); + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Rolling back MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, + ); + throw error; + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts index be48499a10..3b6446ebf1 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts @@ -4,9 +4,11 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command'; import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command'; import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command'; +import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; +import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; -import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -31,6 +33,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace FeatureFlagModule, ], providers: [ + IdentifyPermissionFlagMetadataCommand, + MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, BackfillCommandMenuItemsCommand, BackfillNavigationMenuItemTypeCommand, BackfillPageLayoutsCommand, @@ -38,6 +42,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace MigrateRichTextToTextCommand, ], exports: [ + IdentifyPermissionFlagMetadataCommand, + MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, BackfillCommandMenuItemsCommand, BackfillNavigationMenuItemTypeCommand, BackfillPageLayoutsCommand, 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 1cd2be1472..262d80c443 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 @@ -30,14 +30,16 @@ import { MigrateWorkspacePicturesCommand } from 'src/database/commands/upgrade-v import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command'; import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command'; import { BackfillMissingStandardViewsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-missing-standard-views.command'; -import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command'; import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-system-fields-is-system.command'; import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command'; import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-seed-server-id.command'; import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command'; +import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command'; import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command'; -import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; +import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; +import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; +import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -88,6 +90,8 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly seedServerIdCommand: SeedServerIdCommand, // 1.20 Commands + protected readonly identifyPermissionFlagMetadataCommand: IdentifyPermissionFlagMetadataCommand, + protected readonly makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand, protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand, protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand, @@ -135,10 +139,14 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.addMissingSystemFieldsToStandardObjectsCommand, this.backfillMessageChannelMessageAssociationMessageFolderCommand, this.backfillMissingStandardViewsCommand, + this.fixRoleAndAgentUniversalIdentifiersCommand, this.seedServerIdCommand, ]; const commands_1200: VersionCommands = [ + this.identifyPermissionFlagMetadataCommand, + this + .makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, this.backfillNavigationMenuItemTypeCommand, this.migrateRichTextToTextCommand, this.backfillCommandMenuItemsCommand, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1773232418467-add-universal-identifier-and-application-id-to-permission-flag.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773232418467-add-universal-identifier-and-application-id-to-permission-flag.ts new file mode 100644 index 0000000000..25da0ff90a --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773232418467-add-universal-identifier-and-application-id-to-permission-flag.ts @@ -0,0 +1,25 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddUniversalIdentifierAndApplicationIdToPermissionFlag1773232418467 + implements MigrationInterface +{ + name = 'AddUniversalIdentifierAndApplicationIdToPermissionFlag1773232418467'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ADD "universalIdentifier" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ADD "applicationId" uuid`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" DROP COLUMN IF EXISTS "applicationId"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" DROP COLUMN IF EXISTS "universalIdentifier"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1773232418468-make-permission-flag-universal-identifier-and-application-id-not-null.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773232418468-make-permission-flag-universal-identifier-and-application-id-not-null.ts new file mode 100644 index 0000000000..0363991602 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773232418468-make-permission-flag-universal-identifier-and-application-id-not-null.ts @@ -0,0 +1,58 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +import { makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773232418467-make-permission-flag-universal-identifier-and-application-id-not-null.util'; + +export class MakePermissionFlagUniversalIdentifierAndApplicationIdNotNull1773232418468 + implements MigrationInterface +{ + name = + 'MakePermissionFlagUniversalIdentifierAndApplicationIdNotNull1773232418468'; + + public async up(queryRunner: QueryRunner): Promise { + const savepointName = + 'sp_make_permission_flag_universal_identifier_and_application_id_not_null'; + + try { + await queryRunner.query(`SAVEPOINT ${savepointName}`); + + await makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries( + 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) { + // oxlint-disable-next-line no-console + console.error( + 'Failed to rollback to savepoint in MakePermissionFlagUniversalIdentifierAndApplicationIdNotNull1773232418468', + rollbackError, + ); + throw rollbackError; + } + + // oxlint-disable-next-line no-console + console.error( + 'Swallowing MakePermissionFlagUniversalIdentifierAndApplicationIdNotNull1773232418468 error', + e, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT IF EXISTS "FK_b26a9d39a88d0e72373c677c6c5"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."IDX_da8ffd3c24b4a819430a861067"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ALTER COLUMN "applicationId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ALTER COLUMN "universalIdentifier" DROP NOT NULL`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/utils/1773232418467-make-permission-flag-universal-identifier-and-application-id-not-null.util.ts b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1773232418467-make-permission-flag-universal-identifier-and-application-id-not-null.util.ts new file mode 100644 index 0000000000..9a72a187b0 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1773232418467-make-permission-flag-universal-identifier-and-application-id-not-null.util.ts @@ -0,0 +1,20 @@ +import { type QueryRunner } from 'typeorm'; + +export const makePermissionFlagUniversalIdentifierAndApplicationIdNotNullQueries = + async (queryRunner: QueryRunner): Promise => { + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ALTER COLUMN "universalIdentifier" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ALTER COLUMN "applicationId" SET NOT NULL`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."IDX_da8ffd3c24b4a819430a861067"`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_da8ffd3c24b4a819430a861067" ON "core"."permissionFlag" ("workspaceId", "universalIdentifier")`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ADD CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + }; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util.ts index f1105b8a7e..27426438f3 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util.ts @@ -30,6 +30,7 @@ export const fromRoleManifestToUniversalFlatRole = ({ canBeAssignedToAgents: roleManifest.canBeAssignedToAgents ?? true, canBeAssignedToApiKeys: roleManifest.canBeAssignedToApiKeys ?? true, roleTargetUniversalIdentifiers: [], + permissionFlagUniversalIdentifiers: [], rowLevelPermissionPredicateUniversalIdentifiers: [], rowLevelPermissionPredicateGroupUniversalIdentifiers: [], createdAt: now, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index 1c1d38434c..6f4551bcd6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -178,6 +178,13 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "overrides", ], }, + "permissionFlag": { + "propertiesToCompare": [ + "flag", + "roleUniversalIdentifier", + ], + "propertiesToStringify": [], + }, "role": { "propertiesToCompare": [ "label", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index 9b08bb61f6..bb4a57a905 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -1134,6 +1134,28 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: 'targetObjectMetadataUniversalIdentifier', }, }, + permissionFlag: { + flag: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + roleId: { + toCompare: true, + toStringify: false, + universalProperty: 'roleUniversalIdentifier', + }, + createdAt: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + updatedAt: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + }, rowLevelPermissionPredicate: { fieldMetadataId: { toCompare: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts index 56580796fe..32c974446b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts @@ -163,6 +163,13 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = { workspace: null, application: null, }, + permissionFlag: { + workspace: null, + application: null, + role: { + foreignKey: 'roleId', + }, + }, pageLayout: { workspace: null, objectMetadata: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts index f489beb8c1..bce15f200b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts @@ -280,6 +280,17 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = { workspace: null, application: null, }, + permissionFlag: { + workspace: null, + application: null, + role: { + metadataName: 'role', + foreignKey: 'roleId', + inverseOneToManyProperty: 'permissionFlags', + isNullable: false, + universalForeignKey: 'roleUniversalIdentifier', + }, + }, pageLayout: { workspace: null, objectMetadata: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts index 66293ac247..ac37c2301e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts @@ -12,6 +12,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; @@ -49,6 +50,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = { agent: AgentEntity, commandMenuItem: CommandMenuItemEntity, navigationMenuItem: NavigationMenuItemEntity, + permissionFlag: PermissionFlagEntity, webhook: WebhookEntity, viewSort: ViewSortEntity, } as const satisfies Record>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts index 8a1a9dc4a5..6699329319 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts @@ -74,6 +74,9 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { objectMetadata: true, view: true, }, + permissionFlag: { + role: true, + }, pageLayout: { objectMetadata: true, pageLayoutTab: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts index bdf305018f..34c6347314 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts @@ -33,6 +33,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = { logicFunction: {}, role: {}, roleTarget: {}, + permissionFlag: {}, pageLayout: {}, pageLayoutTab: {}, pageLayoutWidget: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts index 6888531173..b5e8431376 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts @@ -145,7 +145,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { universalFlatEntityForeignKeyAggregator: 'roleTargetUniversalIdentifiers', }, objectPermissions: null, - permissionFlags: null, + permissionFlags: { + metadataName: 'permissionFlag', + flatEntityForeignKeyAggregator: 'permissionFlagIds', + universalFlatEntityForeignKeyAggregator: + 'permissionFlagUniversalIdentifiers', + }, fieldPermissions: null, rowLevelPermissionPredicates: { metadataName: 'rowLevelPermissionPredicate', @@ -161,6 +166,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { }, }, roleTarget: {}, + permissionFlag: {}, pageLayout: { tabs: { metadataName: 'pageLayoutTab', diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts index fc2019123d..4f326a37f2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts @@ -25,6 +25,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; @@ -36,6 +37,7 @@ import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; +import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service'; import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service'; @Module({ @@ -56,6 +58,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module PageLayoutEntity, PageLayoutTabEntity, PageLayoutWidgetEntity, + PermissionFlagEntity, RowLevelPermissionPredicateEntity, RowLevelPermissionPredicateGroupEntity, ApplicationEntity, @@ -74,6 +77,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module WorkspaceFlatIndexMapCacheService, WorkspaceFlatFieldMetadataMapCacheService, WorkspaceFlatViewGroupMapCacheService, + WorkspaceFlatPermissionFlagMapCacheService, WorkspaceFlatViewSortMapCacheService, WorkspaceFlatPageLayoutMapCacheService, WorkspaceFlatPageLayoutTabMapCacheService, @@ -92,6 +96,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module WorkspaceFlatIndexMapCacheService, WorkspaceFlatFieldMetadataMapCacheService, WorkspaceFlatViewGroupMapCacheService, + WorkspaceFlatPermissionFlagMapCacheService, WorkspaceFlatViewSortMapCacheService, WorkspaceFlatPageLayoutMapCacheService, WorkspaceFlatPageLayoutTabMapCacheService, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts index 31da675ab0..120e9a553f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts @@ -15,6 +15,8 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type'; import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type'; +import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type'; +import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type'; import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; @@ -54,6 +56,7 @@ import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/wo import { type UniversalFlatNavigationMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-navigation-menu-item.type'; import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type'; +import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type'; import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type'; import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type'; import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type'; @@ -133,6 +136,14 @@ import { type UniversalDeleteObjectAction, type UniversalUpdateObjectAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action'; +import { + type FlatCreatePermissionFlagAction, + type FlatDeletePermissionFlagAction, + type FlatUpdatePermissionFlagAction, + type UniversalCreatePermissionFlagAction, + type UniversalDeletePermissionFlagAction, + type UniversalUpdatePermissionFlagAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type'; import { type FlatCreatePageLayoutTabAction, type FlatDeletePageLayoutTabAction, @@ -570,6 +581,22 @@ export type AllFlatEntityTypesByMetadataName = { universalFlatEntity: UniversalFlatNavigationMenuItem; entity: NavigationMenuItemEntity; }; + permissionFlag: { + flatEntityMaps: FlatPermissionFlagMaps; + universalActions: { + create: UniversalCreatePermissionFlagAction; + update: UniversalUpdatePermissionFlagAction; + delete: UniversalDeletePermissionFlagAction; + }; + flatActions: { + create: FlatCreatePermissionFlagAction; + update: FlatUpdatePermissionFlagAction; + delete: FlatDeletePermissionFlagAction; + }; + flatEntity: FlatPermissionFlag; + universalFlatEntity: UniversalFlatPermissionFlag; + entity: MetadataEntity<'permissionFlag'>; + }; pageLayout: { flatEntityMaps: FlatPageLayoutMaps; universalActions: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap index d662157437..633bc1e6ec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap @@ -67,9 +67,16 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo ] `; +exports[`getMetadataRelatedMetadataNames should return related metadata names for permissionFlag 1`] = ` +[ + "role", +] +`; + exports[`getMetadataRelatedMetadataNames should return related metadata names for role 1`] = ` [ "roleTarget", + "permissionFlag", "rowLevelPermissionPredicate", "rowLevelPermissionPredicateGroup", ] diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap index c0e8d37350..5cb19fa43f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap @@ -11,6 +11,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with "viewSort", "index", "pageLayout", + "permissionFlag", "roleTarget", "rowLevelPermissionPredicateGroup", "viewGroup", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/constants/flat-permission-flag-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/constants/flat-permission-flag-editable-properties.constant.ts new file mode 100644 index 0000000000..35775f4b21 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/constants/flat-permission-flag-editable-properties.constant.ts @@ -0,0 +1,6 @@ +import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant'; + +export const FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES = [ + 'flag', + 'roleId', +] as const satisfies MetadataEntityPropertyName<'permissionFlag'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service.ts new file mode 100644 index 0000000000..8b890cf336 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type'; +import { fromPermissionFlagEntityToFlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; + +@Injectable() +@WorkspaceCache('flatPermissionFlagMaps') +export class WorkspaceFlatPermissionFlagMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(PermissionFlagEntity) + private readonly permissionFlagRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + ) { + super(); + } + + async computeForCache(workspaceId: string): Promise { + const [permissionFlags, applications, roles] = await Promise.all([ + this.permissionFlagRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.roleRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const roleIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(roles); + + const flatPermissionFlagMaps = createEmptyFlatEntityMaps(); + + for (const permissionFlagEntity of permissionFlags) { + const flatPermissionFlag = fromPermissionFlagEntityToFlatPermissionFlag({ + entity: permissionFlagEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + }); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatPermissionFlag, + flatEntityMapsToMutate: flatPermissionFlagMaps, + }); + } + + return flatPermissionFlagMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type.ts new file mode 100644 index 0000000000..307f46edbe --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type'; + +export type FlatPermissionFlagMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type.ts new file mode 100644 index 0000000000..db2f265cd5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; +import { type PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; + +export type FlatPermissionFlag = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util.ts new file mode 100644 index 0000000000..8e423abbf5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util.ts @@ -0,0 +1,39 @@ +import { v4 } from 'uuid'; + +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; +import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input'; +import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type'; + +export const fromCreatePermissionFlagInputToFlatPermissionFlagToCreate = ({ + createPermissionFlagInput, + flatApplication, + flatRoleMaps, +}: { + createPermissionFlagInput: CreatePermissionFlagInput; + flatApplication: FlatApplication; +} & Pick): UniversalFlatPermissionFlag & { + id: string; +} => { + const { roleId, flag, universalIdentifier } = createPermissionFlagInput; + const now = new Date().toISOString(); + + const { roleUniversalIdentifier } = resolveEntityRelationUniversalIdentifiers( + { + metadataName: 'permissionFlag', + foreignKeyValues: { roleId }, + flatEntityMaps: { flatRoleMaps }, + }, + ); + + return { + id: v4(), + flag, + universalIdentifier: universalIdentifier ?? v4(), + applicationUniversalIdentifier: flatApplication.universalIdentifier, + roleUniversalIdentifier, + createdAt: now, + updatedAt: now, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util.ts new file mode 100644 index 0000000000..8c7a0d079c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util.ts @@ -0,0 +1,53 @@ +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util'; +import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromPermissionFlagEntityToFlatPermissionFlag = ({ + entity: permissionFlagEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'permissionFlag'>): FlatPermissionFlag => { + const permissionFlagEntityWithoutRelations = removePropertiesFromRecord( + permissionFlagEntity, + getMetadataEntityRelationProperties('permissionFlag'), + ); + + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + permissionFlagEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${permissionFlagEntity.applicationId} not found for permissionFlag ${permissionFlagEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get( + permissionFlagEntity.roleId, + ); + + if (!isDefined(roleUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Role with id ${permissionFlagEntity.roleId} not found for permissionFlag ${permissionFlagEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return { + ...permissionFlagEntityWithoutRelations, + createdAt: permissionFlagEntity.createdAt.toISOString(), + updatedAt: permissionFlagEntity.updatedAt.toISOString(), + universalIdentifier: + permissionFlagEntityWithoutRelations.universalIdentifier, + applicationUniversalIdentifier, + roleUniversalIdentifier, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts index e2539f9f07..28df0e4e14 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts @@ -55,6 +55,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({ roleTargetUniversalIdentifiers: [], objectPermissionIds: [], permissionFlagIds: [], + permissionFlagUniversalIdentifiers: [], fieldPermissionIds: [], rowLevelPermissionPredicateIds: [], rowLevelPermissionPredicateUniversalIdentifiers: [], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts index 3edc353c8b..d4c2109e57 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts @@ -68,6 +68,9 @@ export const fromRoleEntityToFlatRole = ({ roleTargetUniversalIdentifiers: roleEntity.roleTargets.map( ({ universalIdentifier }) => universalIdentifier, ), + permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map( + ({ universalIdentifier }) => universalIdentifier, + ), rowLevelPermissionPredicateUniversalIdentifiers: roleEntity.rowLevelPermissionPredicates.map( ({ universalIdentifier }) => universalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input.ts new file mode 100644 index 0000000000..e5d463866d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input.ts @@ -0,0 +1,7 @@ +import { PermissionFlagType } from 'twenty-shared/constants'; + +export type CreatePermissionFlagInput = { + roleId: string; + flag: PermissionFlagType; + universalIdentifier?: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts index 7e6571d5df..b1003527a9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts @@ -12,11 +12,11 @@ import { } from 'typeorm'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @Entity('permissionFlag') @Unique('IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId']) -export class PermissionFlagEntity extends WorkspaceRelatedEntity { +export class PermissionFlagEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts index aaf9ccb867..f77d76940d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts @@ -1,17 +1,18 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; +import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ imports: [ - TypeOrmModule.forFeature([PermissionFlagEntity, RoleEntity]), WorkspaceCacheModule, + ApplicationModule, + WorkspaceMigrationModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, ], - providers: [PermissionFlagService], exports: [PermissionFlagService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts index 88b1a1abd8..192393ab09 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts @@ -1,27 +1,28 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; -import { DataSource, In, Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type'; +import { fromCreatePermissionFlagInputToFlatPermissionFlagToCreate } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util'; import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input'; -import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +@Injectable() export class PermissionFlagService { constructor( - @InjectRepository(RoleEntity) - private readonly roleRepository: Repository, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly applicationService: ApplicationService, ) {} public async upsertPermissionFlags({ @@ -30,11 +31,29 @@ export class PermissionFlagService { }: { workspaceId: string; input: UpsertPermissionFlagsInput; - }): Promise { - await this.validateRoleIsEditableOrThrow({ - roleId: input.roleId, - workspaceId, - }); + }): Promise { + const { flatPermissionFlagMaps, flatRoleMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatPermissionFlagMaps', 'flatRoleMaps'], + }, + ); + + const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId]; + const role = isDefined(roleUniversalId) + ? flatRoleMaps.byUniversalIdentifier[roleUniversalId] + : undefined; + + if (!isDefined(role)) { + throw new PermissionsException( + PermissionsExceptionMessage.ROLE_NOT_FOUND, + PermissionsExceptionCode.ROLE_NOT_FOUND, + { + userFriendlyMessage: msg`The role you are trying to modify could not be found.`, + }, + ); + } const invalidFlags = input.permissionFlagKeys.filter( (flag) => !Object.values(PermissionFlagType).includes(flag), @@ -50,112 +69,85 @@ export class PermissionFlagService { ); } - const queryRunner = this.coreDataSource.createQueryRunner(); + const roleUniversalIdentifier = role.universalIdentifier; - await queryRunner.connect(); - await queryRunner.startTransaction(); + const currentPermissionFlagsForRole = Object.values( + flatPermissionFlagMaps.byUniversalIdentifier, + ).filter( + (pf): pf is FlatPermissionFlag => + isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier, + ); - try { - const existingPermissions = await queryRunner.manager.find( - PermissionFlagEntity, + const inputSet = new Set(input.permissionFlagKeys); + const existingSet = new Set( + currentPermissionFlagsForRole.map((pf) => pf.flag), + ); + + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const flatEntityToCreate = input.permissionFlagKeys + .filter((flag) => !existingSet.has(flag)) + .map((flag) => + fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({ + createPermissionFlagInput: { roleId: input.roleId, flag }, + flatApplication: workspaceCustomFlatApplication, + flatRoleMaps, + }), + ); + + const flatEntityToDelete = currentPermissionFlagsForRole.filter( + (pf) => !inputSet.has(pf.flag), + ); + + if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) { + const unchanged = currentPermissionFlagsForRole.filter((pf) => + inputSet.has(pf.flag), + ); + return unchanged; + } + + const buildAndRunResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { - where: { - roleId: input.roleId, - workspaceId, - }, - }, - ); - const existingSettings = new Set(existingPermissions.map((p) => p.flag)); - const inputSettings = new Set(input.permissionFlagKeys); - - const flagsToAdd = input.permissionFlagKeys.filter( - (setting) => !existingSettings.has(setting), - ); - const permissionsToRemove = existingPermissions.filter( - (permission) => !inputSettings.has(permission.flag), - ); - - if (permissionsToRemove.length > 0) { - await queryRunner.manager.delete(PermissionFlagEntity, { - id: In(permissionsToRemove.map((p) => p.id)), - }); - } - - if (flagsToAdd.length > 0) { - const newPermissions = flagsToAdd.map((flag) => - queryRunner.manager.create(PermissionFlagEntity, { - workspaceId, - roleId: input.roleId, - flag, - }), - ); - - await queryRunner.manager.save(PermissionFlagEntity, newPermissions); - } - - await queryRunner.commitTransaction(); - - return queryRunner.manager.find(PermissionFlagEntity, { - where: { roleId: input.roleId, workspaceId }, - order: { flag: 'ASC' }, - }); - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // oxlint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - - if (error.message.includes('violates foreign key constraint')) { - const role = await this.roleRepository.findOne({ - where: { id: input.roleId }, - }); - - if (!isDefined(role)) { - throw new PermissionsException( - PermissionsExceptionMessage.ROLE_NOT_FOUND, - PermissionsExceptionCode.ROLE_NOT_FOUND, - { - userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`, + allFlatEntityOperationByMetadataName: { + permissionFlag: { + flatEntityToCreate, + flatEntityToDelete, + flatEntityToUpdate: [], }, - ); - } - } - throw error; - } finally { - await queryRunner.release(); - - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - 'rolesPermissions', - ]); - } - } - - private async validateRoleIsEditableOrThrow({ - roleId, - workspaceId, - }: { - roleId: string; - workspaceId: string; - }) { - const role = await this.roleRepository.findOne({ - where: { - id: roleId, - workspaceId, - }, - }); - - if (!role?.isEditable) { - throw new PermissionsException( - PermissionsExceptionMessage.ROLE_NOT_EDITABLE, - PermissionsExceptionCode.ROLE_NOT_EDITABLE, - { - userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, }, ); + + if (buildAndRunResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + buildAndRunResult, + 'Validation errors occurred while upserting permission flags', + ); } + + const { flatPermissionFlagMaps: freshFlatPermissionFlagMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatPermissionFlagMaps'], + }, + ); + + const resultFlags = Object.values( + freshFlatPermissionFlagMaps.byUniversalIdentifier, + ).filter( + (pf): pf is FlatPermissionFlag => + isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier, + ); + + return resultFlags; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util.ts new file mode 100644 index 0000000000..97e3bb8cb3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util.ts @@ -0,0 +1,10 @@ +import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type'; +import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto'; + +export const fromFlatPermissionFlagToPermissionFlagDto = ( + flatPermissionFlag: FlatPermissionFlag, +): PermissionFlagDTO => ({ + id: flatPermissionFlag.id, + roleId: flatPermissionFlag.roleId, + flag: flatPermissionFlag.flag, +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts index 6fef0aa1f2..8c301adce1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts @@ -40,6 +40,7 @@ import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto'; +import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util'; import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input'; import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; import { @@ -222,10 +223,12 @@ export class RoleResolver { @Args('upsertPermissionFlagsInput') upsertPermissionFlagsInput: UpsertPermissionFlagsInput, ): Promise { - return this.settingPermissionService.upsertPermissionFlags({ - workspaceId: workspace.id, - input: upsertPermissionFlagsInput, - }); + const flatPermissionFlags = + await this.settingPermissionService.upsertPermissionFlags({ + workspaceId: workspace.id, + input: upsertPermissionFlagsInput, + }); + return flatPermissionFlags.map(fromFlatPermissionFlagToPermissionFlagDto); } @Mutation(() => [FieldPermissionDTO]) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts index 6e0224017f..6bf09b448b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts @@ -82,7 +82,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< }), this.permissionFlagRepository.find({ where: { workspaceId }, - select: ['id', 'roleId'], + select: ['id', 'universalIdentifier', 'roleId'], withDeleted: true, }), this.fieldPermissionRepository.find({ diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index 25b0036598..b017fac80a 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -32,6 +32,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { flatApplicationMaps: 'flat-maps:flatApplicationMaps', flatRoleMaps: 'flat-maps:role', flatRoleTargetMaps: 'flat-maps:role-target', + flatPermissionFlagMaps: 'flat-maps:permission-flag', ORMEntityMetadatas: 'orm:entity-metadatas', flatAgentMaps: 'flat-maps:agent', flatSkillMaps: 'flat-maps:skill', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts index 7d7810d384..41d9aca6da 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts @@ -73,6 +73,7 @@ export const createStandardRoleFlatMetadata = ({ createdAt: now, updatedAt: now, permissionFlagIds: [], + permissionFlagUniversalIdentifiers: [], fieldPermissionIds: [], objectPermissionIds: [], roleTargetIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts index 015b0cb61c..810759e97a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts @@ -167,6 +167,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ case 'navigationMenuItem': case 'frontComponent': case 'viewSort': + case 'permissionFlag': case 'webhook': { if (!isDefined(idByUniversalIdentifier)) { return action; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index 5222fb0e5b..74dfe55103 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -24,6 +24,7 @@ import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/worksp import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service'; import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service'; import { WorkspaceMigrationPageLayoutActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/workspace-migration-page-layout-actions-builder.service'; +import { WorkspaceMigrationPermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service'; import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-target/workspace-migration-role-target-actions-builder.service'; import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service'; import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service'; @@ -34,9 +35,9 @@ import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/wor import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service'; import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service'; import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; +import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; -import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; @Injectable() export class WorkspaceMigrationBuildOrchestratorService { @@ -50,6 +51,7 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationViewGroupActionsBuilderService: WorkspaceMigrationViewGroupActionsBuilderService, private readonly workspaceMigrationViewFieldGroupActionsBuilderService: WorkspaceMigrationViewFieldGroupActionsBuilderService, private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService, + private readonly workspaceMigrationPermissionFlagActionsBuilderService: WorkspaceMigrationPermissionFlagActionsBuilderService, private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService, private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService, private readonly workspaceMigrationFieldActionsBuilderService: WorkspaceMigrationFieldActionsBuilderService, @@ -140,6 +142,7 @@ export class WorkspaceMigrationBuildOrchestratorService { flatRowLevelPermissionPredicateMaps, flatRowLevelPermissionPredicateGroupMaps, flatRoleMaps, + flatPermissionFlagMaps, flatRoleTargetMaps, flatAgentMaps, flatSkillMaps, @@ -481,6 +484,31 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatPermissionFlagMaps)) { + const { from: fromFlatPermissionFlagMaps, to: toFlatPermissionFlagMaps } = + flatPermissionFlagMaps; + + const permissionFlagResult = + await this.workspaceMigrationPermissionFlagActionsBuilderService.validateAndBuild( + { + additionalCacheDataMaps, + from: fromFlatPermissionFlagMaps, + to: toFlatPermissionFlagMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, + workspaceId, + }, + ); + + if (permissionFlagResult.status === 'fail') { + orchestratorFailureReport.permissionFlag.push( + ...permissionFlagResult.errors, + ); + } else { + orchestratorActionsReport.permissionFlag = permissionFlagResult.actions; + } + } + if (isDefined(flatRoleTargetMaps)) { const { from: fromFlatRoleTargetMaps, to: toFlatRoleTargetMaps } = flatRoleTargetMaps; @@ -811,6 +839,12 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.roleTarget.update, /// + // Permission flags + ...aggregatedOrchestratorActionsReport.permissionFlag.delete, + ...aggregatedOrchestratorActionsReport.permissionFlag.create, + ...aggregatedOrchestratorActionsReport.permissionFlag.update, + /// + // Agents ...aggregatedOrchestratorActionsReport.agent.delete, ...aggregatedOrchestratorActionsReport.agent.create, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap index bce58c7c3a..1fc12fc5f6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap @@ -28,8 +28,10 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc "widgetUniversalIdentifiers", ], "pageLayoutWidget": [], + "permissionFlag": [], "role": [ "roleTargetUniversalIdentifiers", + "permissionFlagUniversalIdentifiers", "rowLevelPermissionPredicateUniversalIdentifiers", "rowLevelPermissionPredicateGroupUniversalIdentifiers", ], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts index 642443c8d3..074fa882cc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts @@ -37,6 +37,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = { }, commandMenuItem: {}, navigationMenuItem: {}, + permissionFlag: {}, frontComponent: {}, } as const satisfies { [P in AllMetadataName]: Partial<{ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type.ts new file mode 100644 index 0000000000..9cd8debb9d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type.ts @@ -0,0 +1,7 @@ +import { type PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type UniversalFlatPermissionFlag = UniversalFlatEntityFrom< + PermissionFlagEntity, + 'permissionFlag' +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type.ts new file mode 100644 index 0000000000..506cbbe488 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type.ts @@ -0,0 +1,24 @@ +import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type'; +import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type'; +import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type'; +import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type'; +import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type'; +import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type'; + +export type FlatCreatePermissionFlagAction = + BaseFlatCreateWorkspaceMigrationAction<'permissionFlag'>; + +export type FlatUpdatePermissionFlagAction = + BaseFlatUpdateWorkspaceMigrationAction<'permissionFlag'>; + +export type FlatDeletePermissionFlagAction = + BaseFlatDeleteWorkspaceMigrationAction<'permissionFlag'>; + +export type UniversalCreatePermissionFlagAction = + BaseUniversalCreateWorkspaceMigrationAction<'permissionFlag'>; + +export type UniversalUpdatePermissionFlagAction = + BaseUniversalUpdateWorkspaceMigrationAction<'permissionFlag'>; + +export type UniversalDeletePermissionFlagAction = + BaseUniversalDeleteWorkspaceMigrationAction<'permissionFlag'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service.ts new file mode 100644 index 0000000000..d7d19c4419 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service.ts @@ -0,0 +1,120 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service'; +import { UniversalUpdatePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type'; +import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; +import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; + +@Injectable() +export class WorkspaceMigrationPermissionFlagActionsBuilderService extends WorkspaceEntityMigrationBuilderService< + typeof ALL_METADATA_NAME.permissionFlag +> { + constructor( + private readonly flatPermissionFlagValidatorService: FlatPermissionFlagValidatorService, + ) { + super(ALL_METADATA_NAME.permissionFlag); + } + + protected validateFlatEntityCreation( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.permissionFlag + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.permissionFlag, + 'create' + > { + const validationResult = + this.flatPermissionFlagValidatorService.validateFlatPermissionFlagCreation( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatPermissionFlagToValidate } = args; + + return { + status: 'success', + action: { + type: 'create', + metadataName: 'permissionFlag', + flatEntity: flatPermissionFlagToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.permissionFlag + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.permissionFlag, + 'delete' + > { + const validationResult = + this.flatPermissionFlagValidatorService.validateFlatPermissionFlagDeletion( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatPermissionFlagToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete', + metadataName: 'permissionFlag', + universalIdentifier: flatPermissionFlagToValidate.universalIdentifier, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.permissionFlag + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.permissionFlag, + 'update' + > { + const validationResult = + this.flatPermissionFlagValidatorService.validateFlatPermissionFlagUpdate( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { universalIdentifier, flatEntityUpdate } = args; + + const updatePermissionFlagAction: UniversalUpdatePermissionFlagAction = { + type: 'update', + metadataName: 'permissionFlag', + universalIdentifier, + update: flatEntityUpdate, + }; + + return { + status: 'success', + action: updatePermissionFlagAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service.ts new file mode 100644 index 0000000000..67a59b6d10 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service.ts @@ -0,0 +1,249 @@ +import { Injectable } from '@nestjs/common'; + +import { msg, t } from '@lingui/core/macro'; +import { PermissionFlagType } from 'twenty-shared/constants'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; +import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; + +@Injectable() +export class FlatPermissionFlagValidatorService { + validateFlatPermissionFlagCreation({ + flatEntityToValidate: flatPermissionFlagToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps, + flatRoleMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.permissionFlag + >): FailedFlatEntityValidation<'permissionFlag', 'create'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier: flatPermissionFlagToValidate.universalIdentifier, + roleUniversalIdentifier: + flatPermissionFlagToValidate.roleUniversalIdentifier, + }, + metadataName: 'permissionFlag', + type: 'create', + }); + + const existingByUniversalId = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatPermissionFlagToValidate.universalIdentifier, + flatEntityMaps: optimisticFlatPermissionFlagMaps, + }); + + if (isDefined(existingByUniversalId)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Permission flag with universal identifier ${flatPermissionFlagToValidate.universalIdentifier} already exists`, + userFriendlyMessage: msg`Permission flag already exists`, + }); + } + + const referencedRole = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatPermissionFlagToValidate.roleUniversalIdentifier, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(referencedRole)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_FOUND, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + } else if (!referencedRole.isEditable) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_EDITABLE, + message: t`Role is not editable`, + userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`, + }); + } + + const isValidFlag = + isDefined(flatPermissionFlagToValidate.flag) && + Object.values(PermissionFlagType).includes( + flatPermissionFlagToValidate.flag, + ); + + if (!isValidFlag) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Invalid permission flag value`, + userFriendlyMessage: msg`Invalid permission setting`, + }); + } + + const duplicateForSameRole = Object.values( + optimisticFlatPermissionFlagMaps.byUniversalIdentifier, + ).filter( + (pf) => + isDefined(pf) && + pf.roleUniversalIdentifier === + flatPermissionFlagToValidate.roleUniversalIdentifier && + pf.flag === flatPermissionFlagToValidate.flag && + pf.universalIdentifier !== + flatPermissionFlagToValidate.universalIdentifier, + ); + + if (duplicateForSameRole.length > 0) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Permission flag for this role and setting already exists`, + userFriendlyMessage: msg`This permission is already set for the role`, + }); + } + + return validationResult; + } + + validateFlatPermissionFlagUpdate({ + universalIdentifier, + flatEntityUpdate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps, + flatRoleMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.permissionFlag + >): FailedFlatEntityValidation<'permissionFlag', 'update'> { + const existingFlatPermissionFlag = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatPermissionFlagMaps, + }); + + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'permissionFlag', + type: 'update', + }); + + if (!isDefined(existingFlatPermissionFlag)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.PERMISSION_NOT_FOUND, + message: t`Permission flag to update not found`, + userFriendlyMessage: msg`Permission flag not found`, + }); + + return validationResult; + } + + const updatedFlatPermissionFlag = { + ...existingFlatPermissionFlag, + ...flatEntityUpdate, + }; + + const referencedRole = findFlatEntityByUniversalIdentifier({ + universalIdentifier: updatedFlatPermissionFlag.roleUniversalIdentifier, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(referencedRole)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_FOUND, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + } else if (!referencedRole.isEditable) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_EDITABLE, + message: t`Role is not editable`, + userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`, + }); + } + + if (isDefined(flatEntityUpdate.flag)) { + const isValidFlag = Object.values(PermissionFlagType).includes( + flatEntityUpdate.flag as PermissionFlagType, + ); + + if (!isValidFlag) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Invalid permission flag value`, + userFriendlyMessage: msg`Invalid permission setting`, + }); + } + } + + const duplicateForSameRole = Object.values( + optimisticFlatPermissionFlagMaps.byUniversalIdentifier, + ).filter( + (pf) => + isDefined(pf) && + pf.roleUniversalIdentifier === + updatedFlatPermissionFlag.roleUniversalIdentifier && + pf.flag === updatedFlatPermissionFlag.flag && + pf.universalIdentifier !== universalIdentifier, + ); + + if (duplicateForSameRole.length > 0) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Permission flag for this role and setting already exists`, + userFriendlyMessage: msg`This permission is already set for the role`, + }); + } + + return validationResult; + } + + validateFlatPermissionFlagDeletion({ + flatEntityToValidate: { universalIdentifier }, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps, + flatRoleMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.permissionFlag + >): FailedFlatEntityValidation<'permissionFlag', 'delete'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'permissionFlag', + type: 'delete', + }); + + const existingFlatPermissionFlag = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatPermissionFlagMaps, + }); + + if (!isDefined(existingFlatPermissionFlag)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.PERMISSION_NOT_FOUND, + message: t`Permission flag to delete not found`, + userFriendlyMessage: msg`Permission flag not found`, + }); + } else { + const referencedRole = findFlatEntityByUniversalIdentifier({ + universalIdentifier: existingFlatPermissionFlag.roleUniversalIdentifier, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(referencedRole)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_FOUND, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + } else if (!referencedRole.isEditable) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_EDITABLE, + message: t`Role is not editable`, + userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`, + }); + } + } + + return validationResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts index ea8310d6ef..25754c79a6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts @@ -8,25 +8,26 @@ import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manage import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-field-metadata-validator.service'; import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service'; import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-index-metadata-validator.service'; +import { FlatLogicFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-logic-function-validator.service'; import { FlatNavigationMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service'; import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service'; import { FlatPageLayoutTabValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service'; import { FlatPageLayoutValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-validator.service'; import { FlatPageLayoutWidgetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service'; +import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service'; import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-target-validator.service'; import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-validator.service'; import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-group-validator.service'; import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service'; -import { FlatLogicFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-logic-function-validator.service'; import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service'; import { FlatViewFieldGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-group-validator.service'; import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service'; import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-group-validator.service'; import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service'; import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-group-validator.service'; +import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service'; import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service'; import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service'; -import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service'; @Module({ imports: [FeatureFlagModule], @@ -38,6 +39,7 @@ import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/works FlatViewGroupValidatorService, FlatViewFieldGroupValidatorService, FlatViewSortValidatorService, + FlatPermissionFlagValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, FlatObjectMetadataValidatorService, @@ -66,6 +68,7 @@ import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/works FlatViewGroupValidatorService, FlatViewFieldGroupValidatorService, FlatViewSortValidatorService, + FlatPermissionFlagValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, FlatObjectMetadataValidatorService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts index b2966f1c52..d9276bdf54 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts @@ -4,29 +4,30 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service'; import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service'; import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service'; -import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service'; import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service'; +import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service'; import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service'; +import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/workspace-migration-logic-function-actions-builder.service'; +import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service'; import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service'; import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service'; import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service'; import { WorkspaceMigrationPageLayoutActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/workspace-migration-page-layout-actions-builder.service'; +import { WorkspaceMigrationPermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service'; import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-target/workspace-migration-role-target-actions-builder.service'; import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service'; import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service'; import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service'; -import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/workspace-migration-logic-function-actions-builder.service'; import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service'; -import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service'; -import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; import { WorkspaceMigrationViewFieldGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service'; -import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; +import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service'; import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service'; import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; -import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; -import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module'; import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; +import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; +import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; +import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module'; @Module({ imports: [FeatureFlagModule, WorkspaceMigrationBuilderValidatorsModule], @@ -40,6 +41,7 @@ import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/work WorkspaceMigrationViewGroupActionsBuilderService, WorkspaceMigrationViewFieldGroupActionsBuilderService, WorkspaceMigrationViewSortActionsBuilderService, + WorkspaceMigrationPermissionFlagActionsBuilderService, WorkspaceMigrationIndexActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, @@ -67,6 +69,7 @@ import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/work WorkspaceMigrationViewGroupActionsBuilderService, WorkspaceMigrationViewFieldGroupActionsBuilderService, WorkspaceMigrationViewSortActionsBuilderService, + WorkspaceMigrationPermissionFlagActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, WorkspaceMigrationRoleActionsBuilderService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/create-permission-flag-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/create-permission-flag-action-handler.service.ts new file mode 100644 index 0000000000..515d557d24 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/create-permission-flag-action-handler.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; + +import { v4 } from 'uuid'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; +import { + FlatCreatePermissionFlagAction, + UniversalCreatePermissionFlagAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreatePermissionFlagActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create', + 'permissionFlag', +) { + override async transpileUniversalActionToFlatAction({ + action, + allFlatEntityMaps, + flatApplication, + workspaceId, + }: WorkspaceMigrationActionRunnerArgs): Promise { + const { roleId } = resolveUniversalRelationIdentifiersToIds({ + flatEntityMaps: allFlatEntityMaps, + metadataName: action.metadataName, + universalForeignKeyValues: action.flatEntity, + }); + + return { + ...action, + flatEntity: { + ...action.flatEntity, + roleId, + applicationId: flatApplication.id, + id: action.id ?? v4(), + workspaceId, + }, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner } = context; + const { flatEntity } = flatAction; + + await this.insertFlatEntitiesInRepository({ + queryRunner, + flatEntities: [flatEntity], + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/delete-permission-flag-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/delete-permission-flag-action-handler.service.ts new file mode 100644 index 0000000000..21aebd6b4f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/delete-permission-flag-action-handler.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { + FlatDeletePermissionFlagAction, + UniversalDeletePermissionFlagAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeletePermissionFlagActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete', + 'permissionFlag', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return this.transpileUniversalDeleteActionToFlatDeleteAction(context); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + + const permissionFlagRepository = + queryRunner.manager.getRepository( + PermissionFlagEntity, + ); + + await permissionFlagRepository.delete({ + id: flatAction.entityId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/update-permission-flag-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/update-permission-flag-action-handler.service.ts new file mode 100644 index 0000000000..0c260882e2 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/update-permission-flag-action-handler.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util'; +import { + FlatUpdatePermissionFlagAction, + UniversalUpdatePermissionFlagAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class UpdatePermissionFlagActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update', + 'permissionFlag', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, allFlatEntityMaps } = context; + + const flatPermissionFlag = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: allFlatEntityMaps.flatPermissionFlagMaps, + universalIdentifier: action.universalIdentifier, + }); + + const update = resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'permissionFlag', + universalUpdate: action.update, + allFlatEntityMaps, + }); + + return { + type: 'update', + metadataName: 'permissionFlag', + entityId: flatPermissionFlag.id, + update, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + const { entityId, update } = flatAction; + + const permissionFlagRepository = + queryRunner.manager.getRepository( + PermissionFlagEntity, + ); + + await permissionFlagRepository.update( + { id: entityId, workspaceId }, + update, + ); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index 66ad69c91f..037241e014 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -9,15 +9,21 @@ import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/wo import { CreateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service'; import { DeleteCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/delete-command-menu-item-action-handler.service'; import { UpdateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/update-command-menu-item-action-handler.service'; -import { CreateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service'; -import { DeleteNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/delete-navigation-menu-item-action-handler.service'; -import { UpdateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/update-navigation-menu-item-action-handler.service'; import { CreateFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service'; import { DeleteFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/delete-field-action-handler.service'; import { UpdateFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service'; +import { CreateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service'; +import { DeleteFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service'; +import { UpdateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service'; import { CreateIndexActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/services/create-index-action-handler.service'; import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/services/delete-index-action-handler.service'; import { UpdateIndexActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/services/update-index-action-handler.service'; +import { CreateLogicFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service'; +import { DeleteLogicFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/delete-logic-function-action-handler.service'; +import { UpdateLogicFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/update-logic-function-action-handler.service'; +import { CreateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service'; +import { DeleteNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/delete-navigation-menu-item-action-handler.service'; +import { UpdateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/update-navigation-menu-item-action-handler.service'; import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/create-object-action-handler.service'; import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/delete-object-action-handler.service'; import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/update-object-action-handler.service'; @@ -30,6 +36,9 @@ import { UpdatePageLayoutWidgetActionHandlerService } from 'src/engine/workspace import { CreatePageLayoutActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service'; import { DeletePageLayoutActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/delete-page-layout-action-handler.service'; import { UpdatePageLayoutActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/update-page-layout-action-handler.service'; +import { CreatePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/create-permission-flag-action-handler.service'; +import { DeletePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/delete-permission-flag-action-handler.service'; +import { UpdatePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/update-permission-flag-action-handler.service'; import { CreateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/create-role-target-action-handler.service'; import { DeleteRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/delete-role-target-action-handler.service'; import { UpdateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/update-role-target-action-handler.service'; @@ -42,18 +51,9 @@ import { UpdateRowLevelPermissionPredicateGroupActionHandlerService } from 'src/ import { CreateRowLevelPermissionPredicateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/create-row-level-permission-predicate-action-handler.service'; import { DeleteRowLevelPermissionPredicateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/delete-row-level-permission-predicate-action-handler.service'; import { UpdateRowLevelPermissionPredicateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/row-level-permission-predicate/services/update-row-level-permission-predicate-action-handler.service'; -import { CreateLogicFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service'; -import { DeleteLogicFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/delete-logic-function-action-handler.service'; -import { UpdateLogicFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/update-logic-function-action-handler.service'; import { CreateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service'; import { DeleteSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/delete-skill-action-handler.service'; -import { CreateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service'; -import { UpdateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service'; -import { DeleteFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service'; import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/update-skill-action-handler.service'; -import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service'; -import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service'; -import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service'; import { CreateViewFieldGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service'; import { DeleteViewFieldGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/delete-view-field-group-action-handler.service'; import { UpdateViewFieldGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/update-view-field-group-action-handler.service'; @@ -69,12 +69,15 @@ import { UpdateViewFilterActionHandlerService } from 'src/engine/workspace-manag import { CreateViewGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/create-view-group-action-handler.service'; import { DeleteViewGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/delete-view-group-action-handler.service'; import { UpdateViewGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-group/services/update-view-group-action-handler.service'; +import { CreateViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/create-view-sort-action-handler.service'; +import { DeleteViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/delete-view-sort-action-handler.service'; +import { UpdateViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/update-view-sort-action-handler.service'; import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service'; import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service'; import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service'; -import { CreateViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/create-view-sort-action-handler.service'; -import { UpdateViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/update-view-sort-action-handler.service'; -import { DeleteViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/delete-view-sort-action-handler.service'; +import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service'; +import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service'; +import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service'; @Module({ imports: [ @@ -134,6 +137,10 @@ import { DeleteViewSortActionHandlerService } from 'src/engine/workspace-manager DeleteRoleTargetActionHandlerService, UpdateRoleTargetActionHandlerService, + CreatePermissionFlagActionHandlerService, + UpdatePermissionFlagActionHandlerService, + DeletePermissionFlagActionHandlerService, + CreateAgentActionHandlerService, UpdateAgentActionHandlerService, DeleteAgentActionHandlerService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts index 4b626e2d94..48f3440fee 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts @@ -19,6 +19,7 @@ export const METADATA_EVENTS_TO_EMIT = { pageLayoutTab: true, commandMenuItem: true, navigationMenuItem: true, + permissionFlag: true, rowLevelPermissionPredicate: true, rowLevelPermissionPredicateGroup: true, index: true, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts index 209c56acb2..1fbb1d8f06 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts @@ -89,6 +89,7 @@ const deriveAllMetadataEventsFromCreateAction = ( case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'permissionFlag': case 'viewSort': case 'webhook': { return [ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts index 1f6078b1b5..ccc14d28f7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts @@ -53,6 +53,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'permissionFlag': case 'viewSort': case 'webhook': { const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow< diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts index a325a25764..6be03a06d6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts @@ -131,6 +131,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'permissionFlag': case 'viewSort': case 'webhook': { const flatEntityMapsKey = getMetadataFlatEntityMapsKey( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts index 0fc1b9ceb8..a67b92480f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts @@ -68,6 +68,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'permissionFlag': case 'webhook': { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({ flatEntity: flatAction.flatEntity, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts index 73f4224901..6ca5f87ae2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts @@ -40,6 +40,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'permissionFlag': case 'webhook': { const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow< MetadataFlatEntity diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts index 6f5f20f475..e3c7fd435f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts @@ -61,6 +61,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'permissionFlag': case 'webhook': { const flatEntityMapsKey = getMetadataFlatEntityMapsKey( flatAction.metadataName, diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts index e7ee2b07da..4e2bcc2f02 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts @@ -7,7 +7,10 @@ import { PermissionFlagType } from 'twenty-shared/constants'; import { fieldTextMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; const client = request(`http://localhost:${APP_PORT}`); @@ -588,12 +591,19 @@ describe('roles permissions', () => { .expect((res) => { expect(res.body.data).toBeNull(); expect(res.body.errors).toBeDefined(); - expect(res.body.errors[0].message).toBe( - PermissionsExceptionMessage.ROLE_NOT_EDITABLE, - ); expect(res.body.errors[0].extensions.code).toBe( - ErrorCode.FORBIDDEN, + ErrorCode.METADATA_VALIDATION_FAILED, ); + const permissionFlagErrors = + res.body.errors[0].extensions.errors?.permissionFlag ?? []; + const hasRoleNotEditable = permissionFlagErrors.some( + (failure: { errors?: Array<{ code?: string }> }) => + failure.errors?.some( + (err) => + err.code === PermissionsExceptionCode.ROLE_NOT_EDITABLE, + ), + ); + expect(hasRoleNotEditable).toBe(true); }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/permission-flag/__snapshots__/failing-permission-flag-upsert.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/permission-flag/__snapshots__/failing-permission-flag-upsert.integration-spec.ts.snap new file mode 100644 index 0000000000..14ec2270e1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/permission-flag/__snapshots__/failing-permission-flag-upsert.integration-spec.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Permission flag upsert should fail when permissionFlagKeys contains invalid enum value 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + }, + "message": "Value \"INVALID_FLAG\" does not exist in \"PermissionFlagType\" enum.", + "name": "GraphQLError", +} +`; + +exports[`Permission flag upsert should fail when role is not editable (system role) 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "permissionFlag": [ + { + "errors": [ + { + "code": "ROLE_NOT_EDITABLE", + "message": "Role is not editable", + "userFriendlyMessage": "This role cannot be modified because it is a system role. Only custom roles can be edited.", + }, + ], + "flatEntityMinimalInformation": { + "roleUniversalIdentifier": Any, + "universalIdentifier": Any, + }, + "metadataName": "permissionFlag", + "status": "fail", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 permissionFlag", + "summary": { + "permissionFlag": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while upserting permission flags", + "name": "GraphQLError", +} +`; + +exports[`Permission flag upsert should fail when roleId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "ROLE_NOT_FOUND", + "userFriendlyMessage": "The role you are trying to modify could not be found.", + }, + "message": "Role not found", + "name": "NotFoundError", +} +`; + +exports[`Permission flag upsert should fail when roleId is not a valid UUID 1`] = ` +{ + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + "value": "invalid-uuid", + }, + "message": "Invalid UUID: 'invalid-uuid'", + "name": "ValidationError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/permission-flag/failing-permission-flag-upsert.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/permission-flag/failing-permission-flag-upsert.integration-spec.ts new file mode 100644 index 0000000000..0dc275ff9e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/permission-flag/failing-permission-flag-upsert.integration-spec.ts @@ -0,0 +1,138 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { upsertPermissionFlags } from 'test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags.util'; +import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util'; +import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util'; +import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { PermissionFlagType } from 'twenty-shared/constants'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input'; + +type GlobalTestContext = { + editableRoleId: string; + nonEditableRoleId: string; +}; + +type TestContext = { + input: (globalContext: GlobalTestContext) => UpsertPermissionFlagsInput; +}; + +const failingPermissionFlagUpsertTestCases: EachTestingContext[] = + [ + { + title: 'when roleId is not a valid UUID', + context: { + input: () => ({ + roleId: 'invalid-uuid', + permissionFlagKeys: [PermissionFlagType.DATA_MODEL], + }), + }, + }, + { + title: 'when roleId does not exist', + context: { + input: () => ({ + roleId: v4(), + permissionFlagKeys: [PermissionFlagType.DATA_MODEL], + }), + }, + }, + { + title: 'when role is not editable (system role)', + context: { + input: (globalContext) => ({ + roleId: globalContext.nonEditableRoleId, + permissionFlagKeys: [PermissionFlagType.DATA_MODEL], + }), + }, + }, + { + title: 'when permissionFlagKeys contains invalid enum value', + context: { + input: (globalContext) => ({ + roleId: globalContext.editableRoleId, + permissionFlagKeys: ['INVALID_FLAG' as PermissionFlagType], + }), + }, + }, + ]; + +describe('Permission flag upsert should fail', () => { + let editableRoleId: string; + let nonEditableRoleId: string; + + beforeAll(async () => { + const { data: roleData } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Role For Permission Flag Failing', + description: 'Role for permission flag failing tests', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + editableRoleId = roleData?.createOneRole?.id; + jestExpectToBeDefined(editableRoleId); + + const { data: rolesData } = await findRoles({ + expectToFail: false, + gqlFields: ` + id + label + isEditable + `, + }); + + jestExpectToBeDefined(rolesData?.getRoles); + const adminRole = rolesData.getRoles.find( + (role: { label: string; isEditable: boolean }) => + role.label === 'Admin' && role.isEditable === false, + ); + jestExpectToBeDefined(adminRole); + nonEditableRoleId = adminRole.id; + }); + + afterAll(async () => { + if (isDefined(editableRoleId)) { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: editableRoleId }, + }); + } + }); + + it.each(eachTestingContextFilter(failingPermissionFlagUpsertTestCases))( + '$title', + async ({ context }) => { + const globalContext: GlobalTestContext = { + editableRoleId: editableRoleId ?? '', + nonEditableRoleId: nonEditableRoleId ?? '', + }; + const input = context.input(globalContext); + + const { errors } = await upsertPermissionFlags({ + expectToFail: true, + input, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/permission-flag/successful-permission-flag-upsert.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/permission-flag/successful-permission-flag-upsert.integration-spec.ts new file mode 100644 index 0000000000..9db61491e1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/permission-flag/successful-permission-flag-upsert.integration-spec.ts @@ -0,0 +1,118 @@ +import { upsertPermissionFlags } from 'test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags.util'; +import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util'; +import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { PermissionFlagType } from 'twenty-shared/constants'; + +describe('Permission flag upsert should succeed', () => { + let createdRoleId: string; + + beforeAll(async () => { + const { data: roleData } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Role For Permission Flag Success', + description: 'Role for permission flag successful tests', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + const roleId = roleData?.createOneRole?.id; + jestExpectToBeDefined(roleId); + createdRoleId = roleId; + }); + + afterAll(async () => { + if (createdRoleId) { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: createdRoleId }, + }); + } + }); + + it('should upsert with empty permissionFlagKeys', async () => { + const { data } = await upsertPermissionFlags({ + expectToFail: false, + input: { + roleId: createdRoleId, + permissionFlagKeys: [], + }, + }); + + expect(data?.upsertPermissionFlags).toEqual([]); + }); + + it('should upsert with one permission flag', async () => { + const { data } = await upsertPermissionFlags({ + expectToFail: false, + input: { + roleId: createdRoleId, + permissionFlagKeys: [PermissionFlagType.DATA_MODEL], + }, + }); + + expect(data?.upsertPermissionFlags).toHaveLength(1); + expect(data?.upsertPermissionFlags?.[0]).toMatchObject({ + roleId: createdRoleId, + flag: PermissionFlagType.DATA_MODEL, + }); + expect(data?.upsertPermissionFlags?.[0].id).toBeDefined(); + }); + + it('should upsert with multiple permission flags', async () => { + const { data } = await upsertPermissionFlags({ + expectToFail: false, + input: { + roleId: createdRoleId, + permissionFlagKeys: [ + PermissionFlagType.DATA_MODEL, + PermissionFlagType.ROLES, + PermissionFlagType.VIEWS, + ], + }, + }); + + expect(data?.upsertPermissionFlags).toHaveLength(3); + const flags = data?.upsertPermissionFlags?.map((pf) => pf.flag) ?? []; + expect(flags).toContain(PermissionFlagType.DATA_MODEL); + expect(flags).toContain(PermissionFlagType.ROLES); + expect(flags).toContain(PermissionFlagType.VIEWS); + }); + + it('should upsert to remove some flags (replace with subset)', async () => { + const { data } = await upsertPermissionFlags({ + expectToFail: false, + input: { + roleId: createdRoleId, + permissionFlagKeys: [PermissionFlagType.DATA_MODEL], + }, + }); + + expect(data?.upsertPermissionFlags).toHaveLength(1); + expect(data?.upsertPermissionFlags?.[0].flag).toBe( + PermissionFlagType.DATA_MODEL, + ); + }); + + it('should upsert back to empty', async () => { + const { data } = await upsertPermissionFlags({ + expectToFail: false, + input: { + roleId: createdRoleId, + permissionFlagKeys: [], + }, + }); + + expect(data?.upsertPermissionFlags).toEqual([]); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags-query-factory.util.ts new file mode 100644 index 0000000000..3ddb215914 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags-query-factory.util.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input'; + +export type UpsertPermissionFlagsFactoryInput = UpsertPermissionFlagsInput; + +const DEFAULT_PERMISSION_FLAG_GQL_FIELDS = ` + id + roleId + flag +`; + +export const upsertPermissionFlagsQueryFactory = ({ + input, + gqlFields = DEFAULT_PERMISSION_FLAG_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpsertPermissionFlags($upsertPermissionFlagsInput: UpsertPermissionFlagsInput!) { + upsertPermissionFlags(upsertPermissionFlagsInput: $upsertPermissionFlagsInput) { + ${gqlFields} + } + } + `, + variables: { + upsertPermissionFlagsInput: input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags.util.ts b/packages/twenty-server/test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags.util.ts new file mode 100644 index 0000000000..c9aa8e2673 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags.util.ts @@ -0,0 +1,43 @@ +import { + type UpsertPermissionFlagsFactoryInput, + upsertPermissionFlagsQueryFactory, +} from 'test/integration/metadata/suites/permission-flag/utils/upsert-permission-flags-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto'; + +export const upsertPermissionFlags = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + upsertPermissionFlags: PermissionFlagDTO[]; +}> => { + const graphqlOperation = upsertPermissionFlagsQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Permission flags upsert should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Permission flags upsert has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts index c1f247b5ab..7b9db86529 100644 --- a/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts +++ b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts @@ -21,6 +21,7 @@ export const ALL_METADATA_NAME = { pageLayoutTab: 'pageLayoutTab', commandMenuItem: 'commandMenuItem', navigationMenuItem: 'navigationMenuItem', + permissionFlag: 'permissionFlag', frontComponent: 'frontComponent', webhook: 'webhook', } as const;