From 6d4d1a97bc8aaec501589887bfcae51683d67ce0 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:02:18 +0530 Subject: [PATCH] Migrate object permission to syncable entity (#18609) Closes [#2223](https://github.com/twentyhq/core-team-issues/issues/2223) --- .../src/generated-metadata/graphql.ts | 1 + .../hooks/useMetadataErrorHandler.ts | 1 + .../clients/generated/metadata/schema.graphql | 1 + .../src/clients/generated/metadata/schema.ts | 3 +- ...tify-object-permission-metadata.command.ts | 111 +++++ ...ation-id-not-nullable-migration.command.ts | 72 +++ .../1-20-upgrade-version-command.module.ts | 6 + .../upgrade.command.ts | 7 + ...and-application-id-to-object-permission.ts | 26 + ...-identifier-and-application-id-not-null.ts | 58 +++ ...tifier-and-application-id-not-null.util.ts | 20 + .../__mocks__/mockPersonObjectMetadata.ts | 2 + .../application-manifest.resolver.ts | 5 +- ...-to-universal-flat-object-metadata.util.ts | 1 + ...le-manifest-to-universal-flat-role.util.ts | 1 + .../object-record-changed-values.spec.ts | 2 + ...ompare-and-stringify.constant.spec.ts.snap | 11 + ...configuration-by-metadata-name.constant.ts | 42 ++ ...ny-to-one-metadata-foreign-key.constant.ts | 10 + ...many-to-one-metadata-relations.constant.ts | 18 + ...tadata-entity-by-metadata-name.constant.ts | 2 + ...quired-metadata-for-validation.constant.ts | 4 + ...l-metadata-serialized-relation.constant.ts | 1 + ...one-to-many-metadata-relations.constant.ts | 15 +- ...ny-or-all-flat-entity-maps-cache.module.ts | 13 +- .../all-flat-entity-types-by-metadata-name.ts | 91 ++-- ...a-related-metadata-names.util.spec.ts.snap | 9 + ...ata-names-children-first.util.spec.ts.snap | 1 + .../get-flat-object-metadata.mock.ts | 2 + ...-flat-object-metadata-map-cache.service.ts | 81 ++-- ...and-flat-field-metadatas-to-create.util.ts | 1 + ...ata-entity-to-flat-object-metadata.util.ts | 7 + ...permission-editable-properties.constant.ts | 10 + ...lat-object-permission-map-cache.service.ts | 86 ++++ .../types/flat-object-permission-maps.type.ts | 4 + .../types/flat-object-permission.type.ts | 4 + ...o-universal-flat-object-permission.util.ts | 54 +++ ...n-entity-to-flat-object-permission.util.ts | 67 +++ ...-role-input-to-flat-role-to-create.util.ts | 1 + .../from-role-entity-to-flat-role.util.ts | 3 + .../object-permission.service.spec.ts | 249 +++++----- .../dtos/create-object-permission.input.ts | 9 + .../object-permission.entity.ts | 4 +- .../object-permission.module.ts | 6 +- .../object-permission.service.ts | 453 ++++++++++-------- ...ermission-to-object-permission-dto.util.ts | 12 + .../metadata-modules/role/role.resolver.ts | 13 +- .../workspace-flat-role-map-cache.service.ts | 2 +- .../workspace-entity-manager.spec.ts | 2 + ...elation-connect-query-configs.util.spec.ts | 8 + ...event-to-database-batch-event.util.spec.ts | 1 + ...umn-name-to-field-metadata-id.util.spec.ts | 2 + ...tadata-id-to-column-names-map.util.spec.ts | 2 + ...ow-level-permission-predicate.util.spec.ts | 2 + .../types/workspace-cache-key.type.ts | 5 +- ...eate-standard-object-flat-metadata.util.ts | 2 + ...create-standard-role-flat-metadata.util.ts | 3 +- ...orkspace-migration-action-with-ids.util.ts | 1 + ...ce-migration-build-orchestrator.service.ts | 37 ++ ...ggregator-properties.constant.spec.ts.snap | 3 + ...ized-relation-by-metadata-name.constant.ts | 1 + .../universal-flat-object-permission.type.ts | 7 + ...migration-object-permission-action.type.ts | 24 + ...ject-permission-actions-builder.service.ts | 121 +++++ ...lat-object-permission-validator.service.ts | 252 ++++++++++ ...ace-migration-builder-validators.module.ts | 3 + .../workspace-migration-builder.module.ts | 3 + ...bject-permission-action-handler.service.ts | 64 +++ ...bject-permission-action-handler.service.ts | 50 ++ ...bject-permission-action-handler.service.ts | 71 +++ ...t-metadata-to-flat-object-metadata.util.ts | 1 + ...migration-runner-action-handlers.module.ts | 7 + .../metadata-event-to-emit.constant.ts | 1 + ...metadata-events-from-create-action.util.ts | 1 + ...metadata-events-from-delete-action.util.ts | 1 + ...metadata-events-from-update-action.util.ts | 1 + ...ate-action-on-all-flat-entity-maps.util.ts | 1 + ...ete-action-on-all-flat-entity-maps.util.ts | 1 + ...ate-action-on-all-flat-entity-maps.util.ts | 3 +- .../roles.integration-spec.ts | 15 +- ...permission-upsert.integration-spec.ts.snap | 101 ++++ ...ject-permission-upsert.integration-spec.ts | 275 +++++++++++ ...ject-permission-upsert.integration-spec.ts | 146 ++++++ ...t-object-permissions-query-factory.util.ts | 34 ++ .../utils/upsert-object-permissions.util.ts | 44 ++ .../constants/all-metadata-name.constant.ts | 1 + 86 files changed, 2475 insertions(+), 424 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160558-add-universal-identifier-and-application-id-to-object-permission.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160559-make-object-permission-universal-identifier-and-application-id-not-null.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-permission/constants/flat-object-permission-editable-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/create-object-permission.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-actions-builder.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-validator.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/create-object-permission-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/delete-object-permission-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/update-object-permission-action-handler.service.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/object-permission/__snapshots__/failing-object-permission-upsert.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/object-permission/failing-object-permission-upsert.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/object-permission/successful-object-permission-upsert.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index c86036bf70..ba2ba4dd1e 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -256,6 +256,7 @@ export enum AllMetadataName { logicFunction = 'logicFunction', navigationMenuItem = 'navigationMenuItem', objectMetadata = 'objectMetadata', + objectPermission = 'objectPermission', pageLayout = 'pageLayout', pageLayoutTab = 'pageLayoutTab', pageLayoutWidget = 'pageLayoutWidget', 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 fdfc04edd0..e468612f71 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 @@ -31,6 +31,7 @@ export const useMetadataErrorHandler = () => { index: t`index`, logicFunction: t`logic function`, permissionFlag: t`permission flag`, + objectPermission: t`object permission`, 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 42fa256a28..0bbed14d68 100644 --- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql +++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql @@ -2241,6 +2241,7 @@ enum AllMetadataName { commandMenuItem navigationMenuItem permissionFlag + objectPermission 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 7f2c70c326..966ca9ebbe 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' | 'permissionFlag' | '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' | 'objectPermission' | 'frontComponent' | 'webhook' export interface MinimalObjectMetadata { id: Scalars['UUID'] @@ -8788,6 +8788,7 @@ export const enumAllMetadataName = { commandMenuItem: 'commandMenuItem' as const, navigationMenuItem: 'navigationMenuItem' as const, permissionFlag: 'permissionFlag' as const, + objectPermission: 'objectPermission' 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-object-permission-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts new file mode 100644 index 0000000000..1155fb3c50 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command.ts @@ -0,0 +1,111 @@ +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 { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +@Command({ + name: 'upgrade:1-20:identify-object-permission-metadata', + description: + 'Identify object permission metadata (backfill universalIdentifier and applicationId)', +}) +export class IdentifyObjectPermissionMetadataCommand 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 IdentifyObjectPermissionMetadataCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const repository = queryRunner.manager.getRepository( + ObjectPermissionEntity, + ); + const withNullApplicationId = await repository.find({ + where: { applicationId: IsNull() }, + relations: ['role'], + }); + + const toUpdate = withNullApplicationId.filter((objectPermission) => + isDefined(objectPermission.role?.applicationId), + ); + const toRemove = withNullApplicationId.filter( + (objectPermission) => !isDefined(objectPermission.role?.applicationId), + ); + + for (const objectPermission of toUpdate) { + objectPermission.applicationId = objectPermission.role!.applicationId; + objectPermission.universalIdentifier = + objectPermission.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 objectPermission of withNullUniversalIdentifier) { + objectPermission.universalIdentifier = v4(); + } + + if (withNullUniversalIdentifier.length > 0) { + await repository.save(withNullUniversalIdentifier); + } + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run IdentifyObjectPermissionMetadataCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Rolling back IdentifyObjectPermissionMetadataCommand: ${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-object-permission-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-object-permission-universal-identifier-and-application-id-not-nullable-migration.command.ts new file mode 100644 index 0000000000..b980ea03a5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-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 { makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-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-object-permission-universal-identifier-and-application-id-not-nullable-migration', + description: + 'Set NOT NULL on objectPermission universalIdentifier and applicationId, add unique index and FK (run identify-object-permission-metadata first)', +}) +export class MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand 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 MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries( + queryRunner, + ); + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + `Rolling back MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${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 3b6446ebf1..b3e35ab4f0 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,7 +4,9 @@ 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 { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command'; import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; +import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.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'; @@ -35,6 +37,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace providers: [ IdentifyPermissionFlagMetadataCommand, MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + IdentifyObjectPermissionMetadataCommand, + MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, BackfillCommandMenuItemsCommand, BackfillNavigationMenuItemTypeCommand, BackfillPageLayoutsCommand, @@ -44,6 +48,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace exports: [ IdentifyPermissionFlagMetadataCommand, MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + IdentifyObjectPermissionMetadataCommand, + MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, 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 262d80c443..8eec277e65 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 @@ -36,7 +36,9 @@ import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-comma 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 { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command'; import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; +import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.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'; @@ -92,6 +94,8 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 1.20 Commands protected readonly identifyPermissionFlagMetadataCommand: IdentifyPermissionFlagMetadataCommand, protected readonly makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + protected readonly identifyObjectPermissionMetadataCommand: IdentifyObjectPermissionMetadataCommand, + protected readonly makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand, protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand, protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand, @@ -147,6 +151,9 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.identifyPermissionFlagMetadataCommand, this .makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, + this.identifyObjectPermissionMetadataCommand, + this + .makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, this.backfillNavigationMenuItemTypeCommand, this.migrateRichTextToTextCommand, this.backfillCommandMenuItemsCommand, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160558-add-universal-identifier-and-application-id-to-object-permission.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160558-add-universal-identifier-and-application-id-to-object-permission.ts new file mode 100644 index 0000000000..fda4cfe79b --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160558-add-universal-identifier-and-application-id-to-object-permission.ts @@ -0,0 +1,26 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddUniversalIdentifierAndApplicationIdToObjectPermission1773317160558 + implements MigrationInterface +{ + name = + 'AddUniversalIdentifierAndApplicationIdToObjectPermission1773317160558'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ADD "universalIdentifier" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ADD "applicationId" uuid`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" DROP COLUMN IF EXISTS "applicationId"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" DROP COLUMN IF EXISTS "universalIdentifier"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160559-make-object-permission-universal-identifier-and-application-id-not-null.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160559-make-object-permission-universal-identifier-and-application-id-not-null.ts new file mode 100644 index 0000000000..c5be59e73b --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1773317160559-make-object-permission-universal-identifier-and-application-id-not-null.ts @@ -0,0 +1,58 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +import { makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util'; + +export class MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559 + implements MigrationInterface +{ + name = + 'MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559'; + + public async up(queryRunner: QueryRunner): Promise { + const savepointName = + 'sp_make_object_permission_universal_identifier_and_application_id_not_null'; + + try { + await queryRunner.query(`SAVEPOINT ${savepointName}`); + + await makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries( + 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 MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559', + rollbackError, + ); + throw rollbackError; + } + + // oxlint-disable-next-line no-console + console.error( + 'Swallowing MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559 error', + e, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" DROP CONSTRAINT IF EXISTS "FK_f2ecee1066fd43800dbc85f87e4"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."IDX_c5ea53618b32558fe24e495f21"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ALTER COLUMN "applicationId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ALTER COLUMN "universalIdentifier" DROP NOT NULL`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util.ts b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util.ts new file mode 100644 index 0000000000..721600a3ac --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util.ts @@ -0,0 +1,20 @@ +import { type QueryRunner } from 'typeorm'; + +export const makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries = + async (queryRunner: QueryRunner): Promise => { + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ALTER COLUMN "universalIdentifier" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ALTER COLUMN "applicationId" SET NOT NULL`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."IDX_c5ea53618b32558fe24e495f21"`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_c5ea53618b32558fe24e495f21" ON "core"."objectPermission" ("workspaceId", "universalIdentifier")`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ADD CONSTRAINT "FK_f2ecee1066fd43800dbc85f87e4" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts index f9b8a508a5..348b17d3f4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts @@ -153,6 +153,7 @@ export const mockPersonFlatObjectMetadata = ( workspaceId, universalIdentifier: objectMetadataId, indexMetadataIds: [], + objectPermissionIds: [], fieldIds: mockFieldMetadatas.map((field) => field.id), viewIds: [], applicationId: 'test-application-id', @@ -168,6 +169,7 @@ export const mockPersonFlatObjectMetadata = ( (field) => field.universalIdentifier, ), viewUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: null, imageIdentifierFieldMetadataUniversalIdentifier: null, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest.resolver.ts index 526205739c..0f4512fe20 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest.resolver.ts @@ -11,10 +11,10 @@ import { FeatureFlagKey } from 'twenty-shared/types'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service'; import { RunWorkspaceMigrationInput } from 'src/engine/core-modules/application/application-manifest/dtos/run-workspace-migration.input'; import { UninstallApplicationInput } from 'src/engine/core-modules/application/application-manifest/dtos/uninstall-application.input'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -25,6 +25,7 @@ import { } from 'src/engine/guards/feature-flag.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service'; @@ -56,7 +57,7 @@ export class ApplicationManifestResolver { await this.workspaceMigrationRunnerService.run({ workspaceMigration: { - actions, + actions: actions as AllUniversalWorkspaceMigrationAction[], applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, }, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts index 4f72465611..e079181b91 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util.ts @@ -35,6 +35,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({ isLabelSyncedWithName: false, fieldUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: objectManifest.labelIdentifierFieldMetadataUniversalIdentifier, 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 27426438f3..9418a1af32 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: [], + objectPermissionUniversalIdentifiers: [], permissionFlagUniversalIdentifiers: [], rowLevelPermissionPredicateUniversalIdentifiers: [], rowLevelPermissionPredicateGroupUniversalIdentifiers: [], diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index 2d878ccce8..f2417dfd0a 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -21,6 +21,7 @@ const mockObjectMetadata: FlatObjectMetadata = { isAuditLogged: true, isSearchable: true, indexMetadataIds: [], + objectPermissionIds: [], fieldIds: [], viewIds: [], applicationId: 'test-application-id', @@ -36,6 +37,7 @@ const mockObjectMetadata: FlatObjectMetadata = { duplicateCriteria: null, applicationUniversalIdentifier: 'test-application-id', fieldUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: null, 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 6f4551bcd6..da709e742a 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 @@ -136,6 +136,17 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "standardOverrides", ], }, + "objectPermission": { + "propertiesToCompare": [ + "roleUniversalIdentifier", + "objectMetadataUniversalIdentifier", + "canReadObjectRecords", + "canUpdateObjectRecords", + "canSoftDeleteObjectRecords", + "canDestroyObjectRecords", + ], + "propertiesToStringify": [], + }, "pageLayout": { "propertiesToCompare": [ "name", 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 bb4a57a905..061aa367ee 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 @@ -1156,6 +1156,48 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: undefined, }, }, + objectPermission: { + roleId: { + toCompare: true, + toStringify: false, + universalProperty: 'roleUniversalIdentifier', + }, + objectMetadataId: { + toCompare: true, + toStringify: false, + universalProperty: 'objectMetadataUniversalIdentifier', + }, + canReadObjectRecords: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + canUpdateObjectRecords: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + canSoftDeleteObjectRecords: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + canDestroyObjectRecords: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + 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 32c974446b..6108ffd66a 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 @@ -170,6 +170,16 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = { foreignKey: 'roleId', }, }, + objectPermission: { + workspace: null, + application: null, + role: { + foreignKey: 'roleId', + }, + objectMetadata: { + foreignKey: 'objectMetadataId', + }, + }, 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 bce15f200b..2d34abc7ee 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 @@ -291,6 +291,24 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = { universalForeignKey: 'roleUniversalIdentifier', }, }, + objectPermission: { + workspace: null, + application: null, + role: { + metadataName: 'role', + foreignKey: 'roleId', + inverseOneToManyProperty: 'objectPermissions', + isNullable: false, + universalForeignKey: 'roleUniversalIdentifier', + }, + objectMetadata: { + metadataName: 'objectMetadata', + foreignKey: 'objectMetadataId', + inverseOneToManyProperty: 'objectPermissions', + isNullable: false, + universalForeignKey: 'objectMetadataUniversalIdentifier', + }, + }, 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 ac37c2301e..8267f99eab 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 @@ -9,6 +9,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/ import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; 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'; @@ -46,6 +47,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = { skill: SkillEntity, logicFunction: LogicFunctionEntity, objectMetadata: ObjectMetadataEntity, + objectPermission: ObjectPermissionEntity, role: RoleEntity, agent: AgentEntity, commandMenuItem: CommandMenuItemEntity, 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 6699329319..cd7457d620 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 @@ -77,6 +77,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { permissionFlag: { role: true, }, + objectPermission: { + role: true, + objectMetadata: 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 34c6347314..3e6a5cd090 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 @@ -34,6 +34,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = { role: {}, roleTarget: {}, permissionFlag: {}, + objectPermission: {}, 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 b5e8431376..661f181a8a 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 @@ -85,7 +85,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { universalFlatEntityForeignKeyAggregator: 'indexMetadataUniversalIdentifiers', }, - objectPermissions: null, + objectPermissions: { + metadataName: 'objectPermission', + flatEntityForeignKeyAggregator: 'objectPermissionIds', + universalFlatEntityForeignKeyAggregator: + 'objectPermissionUniversalIdentifiers', + }, fieldPermissions: null, views: { metadataName: 'view', @@ -144,7 +149,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { flatEntityForeignKeyAggregator: 'roleTargetIds', universalFlatEntityForeignKeyAggregator: 'roleTargetUniversalIdentifiers', }, - objectPermissions: null, + objectPermissions: { + metadataName: 'objectPermission', + flatEntityForeignKeyAggregator: 'objectPermissionIds', + universalFlatEntityForeignKeyAggregator: + 'objectPermissionUniversalIdentifiers', + }, permissionFlags: { metadataName: 'permissionFlag', flatEntityForeignKeyAggregator: 'permissionFlagIds', @@ -167,6 +177,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { }, roleTarget: {}, permissionFlag: {}, + objectPermission: {}, 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 4f326a37f2..b78a0278a7 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 @@ -3,14 +3,15 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service'; import { WorkspaceFlatIndexMapCacheService } from 'src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service'; import { WorkspaceFlatObjectMetadataMapCacheService } from 'src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service'; +import { WorkspaceFlatObjectPermissionMapCacheService } from 'src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service'; import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service'; import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service'; import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service'; +import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service'; import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service'; import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service'; import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service'; @@ -18,10 +19,13 @@ import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modul import { WorkspaceFlatViewFilterGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service'; import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service'; import { WorkspaceFlatViewGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service'; +import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service'; import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service'; +import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; 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'; @@ -34,11 +38,9 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; 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({ imports: [ @@ -55,6 +57,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module IndexFieldMetadataEntity, FieldMetadataEntity, ObjectMetadataEntity, + ObjectPermissionEntity, PageLayoutEntity, PageLayoutTabEntity, PageLayoutWidgetEntity, @@ -77,6 +80,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module WorkspaceFlatIndexMapCacheService, WorkspaceFlatFieldMetadataMapCacheService, WorkspaceFlatViewGroupMapCacheService, + WorkspaceFlatObjectPermissionMapCacheService, WorkspaceFlatPermissionFlagMapCacheService, WorkspaceFlatViewSortMapCacheService, WorkspaceFlatPageLayoutMapCacheService, @@ -96,6 +100,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module WorkspaceFlatIndexMapCacheService, WorkspaceFlatFieldMetadataMapCacheService, WorkspaceFlatViewGroupMapCacheService, + WorkspaceFlatObjectPermissionMapCacheService, WorkspaceFlatPermissionFlagMapCacheService, WorkspaceFlatViewSortMapCacheService, WorkspaceFlatPageLayoutMapCacheService, 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 120e9a553f..e2e9e73bbd 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 @@ -12,14 +12,16 @@ import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-m import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type'; import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { type FlatObjectPermissionMaps } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type'; +import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type'; 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'; +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 FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type'; import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; @@ -55,22 +57,23 @@ import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/wo import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type'; 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 UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.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'; +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 UniversalFlatRoleTarget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-target.type'; -import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type'; +import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type'; import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type'; +import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type'; import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type'; import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter-group.type'; import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type'; import { type UniversalFlatViewGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-group.type'; -import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; +import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; import { type UniversalFlatWebhook } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-webhook.type'; import { type FlatCreateAgentAction, @@ -105,10 +108,10 @@ import { type UniversalUpdateFrontComponentAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type'; import { - type UniversalCreateIndexAction, type FlatCreateIndexAction, type FlatDeleteIndexAction, type FlatUpdateIndexAction, + type UniversalCreateIndexAction, type UniversalDeleteIndexAction, type UniversalUpdateIndexAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/types/workspace-migration-index-action'; @@ -128,6 +131,14 @@ import { type UniversalDeleteNavigationMenuItemAction, type UniversalUpdateNavigationMenuItemAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type'; +import { + type FlatCreateObjectPermissionAction, + type FlatDeleteObjectPermissionAction, + type FlatUpdateObjectPermissionAction, + type UniversalCreateObjectPermissionAction, + type UniversalDeleteObjectPermissionAction, + type UniversalUpdateObjectPermissionAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type'; import { type FlatCreateObjectAction, type FlatDeleteObjectAction, @@ -136,14 +147,6 @@ 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, @@ -168,6 +171,14 @@ import { type UniversalDeletePageLayoutAction, type UniversalUpdatePageLayoutAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type'; +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 FlatCreateRoleTargetAction, type FlatDeleteRoleTargetAction, @@ -208,14 +219,6 @@ import { type UniversalDeleteSkillAction, type UniversalUpdateSkillAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/types/workspace-migration-skill-action.type'; -import { - type FlatCreateViewFieldAction, - type FlatDeleteViewFieldAction, - type FlatUpdateViewFieldAction, - type UniversalCreateViewFieldAction, - type UniversalDeleteViewFieldAction, - type UniversalUpdateViewFieldAction, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type'; import { type FlatCreateViewFieldGroupAction, type FlatDeleteViewFieldGroupAction, @@ -224,6 +227,14 @@ import { type UniversalDeleteViewFieldGroupAction, type UniversalUpdateViewFieldGroupAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type'; +import { + type FlatCreateViewFieldAction, + type FlatDeleteViewFieldAction, + type FlatUpdateViewFieldAction, + type UniversalCreateViewFieldAction, + type UniversalDeleteViewFieldAction, + type UniversalUpdateViewFieldAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type'; import { type FlatCreateViewFilterGroupAction, type FlatDeleteViewFilterGroupAction, @@ -248,6 +259,14 @@ import { type UniversalDeleteViewGroupAction, type UniversalUpdateViewGroupAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/types/workspace-migration-view-group-action.type'; +import { + type FlatCreateViewSortAction, + type FlatDeleteViewSortAction, + type FlatUpdateViewSortAction, + type UniversalCreateViewSortAction, + type UniversalDeleteViewSortAction, + type UniversalUpdateViewSortAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; import { type FlatCreateViewAction, type FlatDeleteViewAction, @@ -264,14 +283,6 @@ import { type UniversalDeleteWebhookAction, type UniversalUpdateWebhookAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type'; -import { - type FlatCreateViewSortAction, - type FlatDeleteViewSortAction, - type FlatUpdateViewSortAction, - type UniversalCreateViewSortAction, - type UniversalDeleteViewSortAction, - type UniversalUpdateViewSortAction, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; export type AllFlatEntityTypesByMetadataName = { fieldMetadata: { @@ -597,6 +608,22 @@ export type AllFlatEntityTypesByMetadataName = { universalFlatEntity: UniversalFlatPermissionFlag; entity: MetadataEntity<'permissionFlag'>; }; + objectPermission: { + flatEntityMaps: FlatObjectPermissionMaps; + universalActions: { + create: UniversalCreateObjectPermissionAction; + update: UniversalUpdateObjectPermissionAction; + delete: UniversalDeleteObjectPermissionAction; + }; + flatActions: { + create: FlatCreateObjectPermissionAction; + update: FlatUpdateObjectPermissionAction; + delete: FlatDeleteObjectPermissionAction; + }; + flatEntity: FlatObjectPermission; + universalFlatEntity: UniversalFlatObjectPermission; + entity: MetadataEntity<'objectPermission'>; + }; 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 633bc1e6ec..201ecc9e84 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 @@ -42,10 +42,18 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo [ "fieldMetadata", "index", + "objectPermission", "view", ] `; +exports[`getMetadataRelatedMetadataNames should return related metadata names for objectPermission 1`] = ` +[ + "role", + "objectMetadata", +] +`; + exports[`getMetadataRelatedMetadataNames should return related metadata names for pageLayout 1`] = ` [ "objectMetadata", @@ -76,6 +84,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo exports[`getMetadataRelatedMetadataNames should return related metadata names for role 1`] = ` [ "roleTarget", + "objectPermission", "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 5cb19fa43f..800b037d47 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 @@ -7,6 +7,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with "viewField", "viewFilter", "commandMenuItem", + "objectPermission", "pageLayoutWidget", "viewSort", "index", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts index 6ed4cb7d56..4f5d1deefa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts @@ -19,6 +19,7 @@ export const getFlatObjectMetadataMock = ( return { viewIds: [], indexMetadataIds: [], + objectPermissionIds: [], fieldIds: [], description: 'default flat object metadata description', icon: 'icon', @@ -48,6 +49,7 @@ export const getFlatObjectMetadataMock = ( duplicateCriteria: null, applicationUniversalIdentifier: applicationId, fieldUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts index 13ff1f3836..a0eb0f61ba 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts @@ -13,6 +13,7 @@ import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-meta import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; 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'; @@ -35,6 +36,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr private readonly indexMetadataRepository: Repository, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, + @InjectRepository(ObjectPermissionEntity) + private readonly objectPermissionRepository: Repository, ) { super(); } @@ -42,35 +45,51 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr async computeForCache( workspaceId: string, ): Promise> { - const [objectMetadatas, applications, fields, indexMetadatas, views] = - await Promise.all([ - this.objectMetadataRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - this.applicationRepository.find({ - where: { workspaceId }, - select: ['id', 'universalIdentifier'], - withDeleted: true, - }), - this.fieldMetadataRepository.find({ - where: { workspaceId }, - select: ['id', 'universalIdentifier', 'objectMetadataId'], - withDeleted: true, - }), - this.indexMetadataRepository.find({ - where: { workspaceId }, - select: ['id', 'universalIdentifier', 'objectMetadataId'], - withDeleted: true, - }), - this.viewRepository.find({ - where: { workspaceId }, - select: ['id', 'universalIdentifier', 'objectMetadataId'], - withDeleted: true, - }), - ]); + const [ + objectMetadatas, + applications, + fields, + indexMetadatas, + views, + objectPermissions, + ] = await Promise.all([ + this.objectMetadataRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + this.indexMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + this.objectPermissionRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + ]); - const [fieldsByObjectId, indexesByObjectId, viewsByObjectId] = ( + const [ + fieldsByObjectId, + indexesByObjectId, + viewsByObjectId, + objectPermissionsByObjectId, + ] = ( [ { entities: fields, @@ -84,6 +103,10 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr entities: views, foreignKey: 'objectMetadataId', }, + { + entities: objectPermissions, + foreignKey: 'objectMetadataId', + }, ] as const ).map(regroupEntitiesByRelatedEntityId); @@ -101,6 +124,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr fields: fieldsByObjectId.get(objectMetadataEntity.id) || [], indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [], views: viewsByObjectId.get(objectMetadataEntity.id) || [], + objectPermissions: + objectPermissionsByObjectId.get(objectMetadataEntity.id) || [], }, applicationIdToUniversalIdentifierMap, fieldMetadataIdToUniversalIdentifierMap, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts index 02adfc9871..663e8d08c9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts @@ -94,6 +94,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre targetTableName: 'DEPRECATED', applicationUniversalIdentifier: flatApplication.universalIdentifier, fieldUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts index acac212b3e..367e2fae93 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts @@ -78,6 +78,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({ viewIds: objectMetadataEntity.views.map(({ id }) => id), indexMetadataIds: objectMetadataEntity.indexMetadatas.map(({ id }) => id), fieldIds: objectMetadataEntity.fields.map(({ id }) => id), + objectPermissionIds: objectMetadataEntity.objectPermissions.map( + ({ id }) => id, + ), applicationUniversalIdentifier, labelIdentifierFieldMetadataUniversalIdentifier, imageIdentifierFieldMetadataUniversalIdentifier, @@ -90,5 +93,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({ viewUniversalIdentifiers: objectMetadataEntity.views.map( ({ universalIdentifier }) => universalIdentifier, ), + objectPermissionUniversalIdentifiers: + objectMetadataEntity.objectPermissions.map( + ({ universalIdentifier }) => universalIdentifier, + ), }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/constants/flat-object-permission-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/constants/flat-object-permission-editable-properties.constant.ts new file mode 100644 index 0000000000..3a9a3c2343 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/constants/flat-object-permission-editable-properties.constant.ts @@ -0,0 +1,10 @@ +import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant'; + +export const FLAT_OBJECT_PERMISSION_EDITABLE_PROPERTIES = [ + 'roleId', + 'objectMetadataId', + 'canReadObjectRecords', + 'canUpdateObjectRecords', + 'canSoftDeleteObjectRecords', + 'canDestroyObjectRecords', +] as const satisfies MetadataEntityPropertyName<'objectPermission'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service.ts new file mode 100644 index 0000000000..04cc497761 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service.ts @@ -0,0 +1,86 @@ +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 FlatObjectPermissionMaps } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type'; +import { fromObjectPermissionEntityToFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +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('flatObjectPermissionMaps') +export class WorkspaceFlatObjectPermissionMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ObjectPermissionEntity) + private readonly objectPermissionRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const [objectPermissions, applications, roles, objectMetadatas] = + await Promise.all([ + this.objectPermissionRepository.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, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const roleIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(roles); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + + const flatObjectPermissionMaps = createEmptyFlatEntityMaps(); + + for (const objectPermissionEntity of objectPermissions) { + const flatObjectPermission = + fromObjectPermissionEntityToFlatObjectPermission({ + entity: objectPermissionEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + }); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatObjectPermission, + flatEntityMapsToMutate: flatObjectPermissionMaps, + }); + } + + return flatObjectPermissionMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type.ts new file mode 100644 index 0000000000..367cca6609 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type'; + +export type FlatObjectPermissionMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type.ts new file mode 100644 index 0000000000..69dbf8f6cb --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; +import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; + +export type FlatObjectPermission = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util.ts new file mode 100644 index 0000000000..4dc78d9343 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util.ts @@ -0,0 +1,54 @@ +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 CreateObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/create-object-permission.input'; +import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type'; + +export const fromCreateObjectPermissionInputToUniversalFlatObjectPermission = ({ + createObjectPermissionInput, + flatApplication, + flatRoleMaps, + flatObjectMetadataMaps, +}: { + createObjectPermissionInput: CreateObjectPermissionInput; + flatApplication: FlatApplication; +} & Pick< + AllFlatEntityMaps, + 'flatRoleMaps' | 'flatObjectMetadataMaps' +>): UniversalFlatObjectPermission & { id: string } => { + const { + roleId, + objectMetadataId, + canReadObjectRecords, + canUpdateObjectRecords, + canSoftDeleteObjectRecords, + canDestroyObjectRecords, + universalIdentifier, + } = createObjectPermissionInput; + const now = new Date().toISOString(); + + const { roleUniversalIdentifier, objectMetadataUniversalIdentifier } = + resolveEntityRelationUniversalIdentifiers({ + metadataName: 'objectPermission', + foreignKeyValues: { roleId, objectMetadataId }, + flatEntityMaps: { + flatRoleMaps, + flatObjectMetadataMaps, + }, + }); + return { + id: v4(), + universalIdentifier: universalIdentifier ?? v4(), + applicationUniversalIdentifier: flatApplication.universalIdentifier, + roleUniversalIdentifier, + objectMetadataUniversalIdentifier, + canReadObjectRecords: canReadObjectRecords ?? undefined, + canUpdateObjectRecords: canUpdateObjectRecords ?? undefined, + canSoftDeleteObjectRecords: canSoftDeleteObjectRecords ?? undefined, + canDestroyObjectRecords: canDestroyObjectRecords ?? undefined, + createdAt: now, + updatedAt: now, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util.ts new file mode 100644 index 0000000000..43b2e056a5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util.ts @@ -0,0 +1,67 @@ +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 FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromObjectPermissionEntityToFlatObjectPermission = ({ + entity: objectPermissionEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'objectPermission'>): FlatObjectPermission => { + const objectPermissionEntityWithoutRelations = removePropertiesFromRecord( + objectPermissionEntity, + getMetadataEntityRelationProperties('objectPermission'), + ); + + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + objectPermissionEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${objectPermissionEntity.applicationId} not found for objectPermission ${objectPermissionEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get( + objectPermissionEntity.roleId, + ); + + if (!isDefined(roleUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Role with id ${objectPermissionEntity.roleId} not found for objectPermission ${objectPermissionEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + objectPermissionEntity.objectMetadataId, + ); + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${objectPermissionEntity.objectMetadataId} not found for objectPermission ${objectPermissionEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return { + ...objectPermissionEntityWithoutRelations, + createdAt: objectPermissionEntity.createdAt.toISOString(), + updatedAt: objectPermissionEntity.updatedAt.toISOString(), + universalIdentifier: + objectPermissionEntityWithoutRelations.universalIdentifier, + applicationUniversalIdentifier, + roleUniversalIdentifier, + objectMetadataUniversalIdentifier, + }; +}; 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 28df0e4e14..7346d5a1a9 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: [], + objectPermissionUniversalIdentifiers: [], permissionFlagUniversalIdentifiers: [], fieldPermissionIds: [], rowLevelPermissionPredicateIds: [], 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 d4c2109e57..e0800831b6 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, ), + objectPermissionUniversalIdentifiers: roleEntity.objectPermissions.map( + ({ universalIdentifier }) => universalIdentifier, + ), permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map( ({ universalIdentifier }) => universalIdentifier, ), diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts index 7e87708279..4317d7d475 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/__tests__/object-permission.service.spec.ts @@ -1,58 +1,67 @@ import { Test, type TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; - -import { type Repository } from 'typeorm'; +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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; -import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; 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 { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; + +const createMockFlatRoleMaps = (roleId: string, isEditable: boolean) => ({ + byUniversalIdentifier: { + [roleId]: { + id: roleId, + universalIdentifier: roleId, + isEditable, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }, + universalIdentifierById: { [roleId]: roleId }, + byId: { [roleId]: { id: roleId, universalIdentifier: roleId, isEditable } }, +}); + +const createMockFlatObjectMetadataMaps = ( + objectMetadataId: string, + isSystem: boolean, +) => ({ + byUniversalIdentifier: { + [objectMetadataId]: { + id: objectMetadataId, + universalIdentifier: objectMetadataId, + isSystem, + }, + }, + universalIdentifierById: { [objectMetadataId]: objectMetadataId }, + byId: { [objectMetadataId]: { id: objectMetadataId, isSystem } }, +}); describe('ObjectPermissionService', () => { let service: ObjectPermissionService; - let objectPermissionRepository: jest.Mocked< - Repository - >; - let roleRepository: jest.Mocked>; - let workspaceCacheService: jest.Mocked; let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked; + let workspaceMigrationValidateBuildAndRunService: jest.Mocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ObjectPermissionService, { - provide: getRepositoryToken(ObjectPermissionEntity), + provide: ApplicationService, useValue: { - upsert: jest.fn(), - find: jest.fn(), - }, - }, - { - provide: getRepositoryToken(RoleEntity), - useValue: { - findOne: jest.fn(), - }, - }, - { - provide: getRepositoryToken(ObjectMetadataEntity), - useValue: { - find: jest.fn(), - }, - }, - { - provide: WorkspaceCacheService, - useValue: { - invalidate: jest.fn(), - invalidateAndRecompute: jest.fn(), + findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest + .fn() + .mockResolvedValue({ + workspaceCustomFlatApplication: { + id: 'app-id', + universalIdentifier: 'app-universal-id', + }, + }), }, }, { @@ -61,18 +70,22 @@ describe('ObjectPermissionService', () => { getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(), }, }, + { + provide: WorkspaceMigrationValidateBuildAndRunService, + useValue: { + validateBuildAndRunWorkspaceMigration: jest.fn(), + }, + }, ], }).compile(); service = module.get(ObjectPermissionService); - objectPermissionRepository = module.get( - getRepositoryToken(ObjectPermissionEntity), - ); - roleRepository = module.get(getRepositoryToken(RoleEntity)); - workspaceCacheService = module.get(WorkspaceCacheService); workspaceManyOrAllFlatEntityMapsCacheService = module.get( WorkspaceManyOrAllFlatEntityMapsCacheService, ); + workspaceMigrationValidateBuildAndRunService = module.get( + WorkspaceMigrationValidateBuildAndRunService, + ); }); describe('upsertObjectPermissions', () => { @@ -81,18 +94,7 @@ describe('ObjectPermissionService', () => { const systemObjectMetadataId = 'system-object-id'; const customObjectMetadataId = 'custom-object-id'; - beforeEach(() => { - // Mock role validation - roleRepository.findOne.mockResolvedValue({ - id: roleId, - workspaceId, - isEditable: true, - objectPermissions: [], - } as unknown as RoleEntity); - }); - it('should throw PermissionsException when trying to add object permission on system object', async () => { - // Arrange const input: UpsertObjectPermissionsInput = { roleId, objectPermissions: [ @@ -106,31 +108,21 @@ describe('ObjectPermissionService', () => { ], }; - // Mock flat object metadata maps with a system object workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue( { - flatObjectMetadataMaps: { - byUniversalIdentifier: { - [systemObjectMetadataId]: { - id: systemObjectMetadataId, - isSystem: true, - workspaceId, - fieldIds: [], - indexMetadataIds: [], - viewIds: [], - universalIdentifier: systemObjectMetadataId, - applicationId: null, - } as any, - }, - universalIdentifierById: { - [systemObjectMetadataId]: systemObjectMetadataId, - }, - universalIdentifiersByApplicationId: {}, + flatObjectPermissionMaps: { + byUniversalIdentifier: {}, + universalIdentifierById: {}, + byId: {}, }, + flatRoleMaps: createMockFlatRoleMaps(roleId, true), + flatObjectMetadataMaps: createMockFlatObjectMetadataMaps( + systemObjectMetadataId, + true, + ), } as any, ); - // Act & Assert await expect( service.upsertObjectPermissions({ workspaceId, @@ -143,15 +135,12 @@ describe('ObjectPermissionService', () => { ), ); - // Verify that upsert was never called - expect(objectPermissionRepository.upsert).not.toHaveBeenCalled(); expect( - workspaceCacheService.invalidateAndRecompute, + workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration, ).not.toHaveBeenCalled(); }); it('should successfully create object permission for custom (non-system) object', async () => { - // Arrange const input: UpsertObjectPermissionsInput = { roleId, objectPermissions: [ @@ -165,82 +154,70 @@ describe('ObjectPermissionService', () => { ], }; - // Mock flat object metadata maps with a custom object - workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue( - { - flatObjectMetadataMaps: { - byUniversalIdentifier: { - [customObjectMetadataId]: { - id: customObjectMetadataId, - isSystem: false, - workspaceId, - fieldIds: [], - indexMetadataIds: [], - viewIds: [], - universalIdentifier: customObjectMetadataId, - applicationId: null, - } as any, - }, - universalIdentifierById: { - [customObjectMetadataId]: customObjectMetadataId, - }, - universalIdentifiersByApplicationId: {}, - }, - } as any, - ); - - // Mock successful upsert - const mockObjectPermission = { + const permissionUniversalId = 'permission-universal-id'; + const freshFlatObjectPermission = { id: 'permission-id', + universalIdentifier: permissionUniversalId, roleId, + roleUniversalIdentifier: roleId, objectMetadataId: customObjectMetadataId, - workspaceId, + objectMetadataUniversalIdentifier: customObjectMetadataId, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, - } as ObjectPermissionEntity; + }; - objectPermissionRepository.upsert.mockResolvedValue({ - generatedMaps: [{ id: 'permission-id' }], - identifiers: [], - raw: [], - }); + workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps + .mockResolvedValueOnce({ + flatObjectPermissionMaps: { + byUniversalIdentifier: {}, + universalIdentifierById: {}, + byId: {}, + }, + flatRoleMaps: createMockFlatRoleMaps(roleId, true), + flatObjectMetadataMaps: createMockFlatObjectMetadataMaps( + customObjectMetadataId, + false, + ), + } as any) + .mockResolvedValueOnce({ + flatObjectPermissionMaps: { + byUniversalIdentifier: { + [permissionUniversalId]: freshFlatObjectPermission, + }, + universalIdentifierById: { + [freshFlatObjectPermission.id]: permissionUniversalId, + }, + byId: { + [freshFlatObjectPermission.id]: freshFlatObjectPermission, + }, + }, + } as any); - objectPermissionRepository.find.mockResolvedValue([mockObjectPermission]); + workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue( + { status: 'success' } as any, + ); - // Act const result = await service.upsertObjectPermissions({ workspaceId, input, }); - // Assert - expect(result).toEqual([mockObjectPermission]); - expect(objectPermissionRepository.upsert).toHaveBeenCalledWith( - [ - { - objectMetadataId: customObjectMetadataId, - canReadObjectRecords: true, - canUpdateObjectRecords: true, - canSoftDeleteObjectRecords: false, - canDestroyObjectRecords: false, - roleId, - workspaceId, - }, - ], - { - conflictPaths: ['objectMetadataId', 'roleId'], - }, - ); - expect(workspaceCacheService.invalidateAndRecompute).toHaveBeenCalledWith( - workspaceId, - ['rolesPermissions'], - ); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }); + expect( + workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration, + ).toHaveBeenCalled(); }); it('should throw PermissionsException when object metadata is not found', async () => { - // Arrange const input: UpsertObjectPermissionsInput = { roleId, objectPermissions: [ @@ -254,18 +231,22 @@ describe('ObjectPermissionService', () => { ], }; - // Mock empty flat object metadata maps workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue( { + flatObjectPermissionMaps: { + byUniversalIdentifier: {}, + universalIdentifierById: {}, + byId: {}, + }, + flatRoleMaps: createMockFlatRoleMaps(roleId, true), flatObjectMetadataMaps: { byUniversalIdentifier: {}, universalIdentifierById: {}, - universalIdentifiersByApplicationId: {}, + byId: {}, }, } as any, ); - // Act & Assert await expect( service.upsertObjectPermissions({ workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/create-object-permission.input.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/create-object-permission.input.ts new file mode 100644 index 0000000000..bf3103ac60 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/create-object-permission.input.ts @@ -0,0 +1,9 @@ +export type CreateObjectPermissionInput = { + roleId: string; + objectMetadataId: string; + canReadObjectRecords?: boolean; + canUpdateObjectRecords?: boolean; + canSoftDeleteObjectRecords?: boolean; + canDestroyObjectRecords?: boolean; + universalIdentifier?: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts index 4bb43d2528..f4ca2be77e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts @@ -13,7 +13,7 @@ import { import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; 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('objectPermission') @Unique('IDX_OBJECT_PERMISSION_OBJECT_METADATA_ID_ROLE_ID_UNIQUE', [ @@ -21,7 +21,7 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works 'roleId', ]) @Index('IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId']) -export class ObjectPermissionEntity extends WorkspaceRelatedEntity { +export class ObjectPermissionEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts index d6217f6941..ed45f4c17a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -10,6 +11,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.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: [ @@ -20,8 +22,10 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache FieldPermissionEntity, FieldMetadataEntity, ]), - WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceCacheModule, + ApplicationModule, + WorkspaceMigrationModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [ObjectPermissionService, FieldPermissionService], exports: [ObjectPermissionService, FieldPermissionService], diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts index 6d1e7c30db..92385599c0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts @@ -1,35 +1,33 @@ -import { InjectRepository } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; -import { In, Repository } from 'typeorm'; +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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type'; +import { fromCreateObjectPermissionInputToUniversalFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util'; +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; import { type ObjectPermissionInput, type UpsertObjectPermissionsInput, } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; -import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.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'; +import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type'; +@Injectable() export class ObjectPermissionService { constructor( - @InjectRepository(ObjectPermissionEntity) - private readonly objectPermissionRepository: Repository, - @InjectRepository(RoleEntity) - private readonly roleRepository: Repository, - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, - private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly applicationService: ApplicationService, ) {} public async upsertObjectPermissions({ @@ -38,144 +36,269 @@ export class ObjectPermissionService { }: { workspaceId: string; input: UpsertObjectPermissionsInput; - }): Promise { - try { - const role = await this.getRoleOrThrow({ - roleId: input.roleId, - workspaceId, + }): Promise { + const { flatObjectPermissionMaps, flatRoleMaps, flatObjectMetadataMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatObjectPermissionMaps', + 'flatRoleMaps', + 'flatObjectMetadataMaps', + ], + }, + ); + + const roleUniversalIdentifier = + flatRoleMaps.universalIdentifierById[input.roleId]; + const flatRole = isDefined(roleUniversalIdentifier) + ? flatRoleMaps.byUniversalIdentifier[roleUniversalIdentifier] + : undefined; + + if (!isDefined(flatRole)) { + 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 currentObjectPermissionsForRole = Object.values( + flatObjectPermissionMaps.byUniversalIdentifier, + ).filter( + (op): op is FlatObjectPermission => + isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier, + ); + + this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({ + objectPermissions: input.objectPermissions, + flatRole, + currentObjectPermissionsForRole, + }); + + const flatApplication = + await this.getFlatApplicationForWorkspace(workspaceId); + + const desiredByObjectMetadataId = new Map( + input.objectPermissions.map((op) => [op.objectMetadataId, op]), + ); + + for (const desired of input.objectPermissions) { + const objectMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: desired.objectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, }); - await this.validateRoleIsEditableOrThrow({ - role, - }); - - await this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({ - objectPermissions: input.objectPermissions, - roleWithObjectPermissions: role, - }); - - const { flatObjectMetadataMaps } = - await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + if (!isDefined(objectMetadata)) { + throw new PermissionsException( + 'Object metadata id not found', + PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps'], + userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`, }, ); - - input.objectPermissions.forEach((objectPermission) => { - const objectMetadataForObjectPermission = - findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: objectPermission.objectMetadataId, - flatEntityMaps: flatObjectMetadataMaps, - }); - - if (!isDefined(objectMetadataForObjectPermission)) { - throw new PermissionsException( - 'Object metadata id not found', - PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, - { - userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`, - }, - ); - } - - if (objectMetadataForObjectPermission.isSystem === true) { - throw new PermissionsException( - PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT, - PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT, - { - userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`, - }, - ); - } - }); - - const objectPermissions = input.objectPermissions.map( - (objectPermission) => ({ - ...objectPermission, - roleId: input.roleId, - workspaceId, - }), - ); - - const result = await this.objectPermissionRepository.upsert( - objectPermissions, - { - conflictPaths: ['objectMetadataId', 'roleId'], - }, - ); - - const objectPermissionId = result.generatedMaps?.[0]?.id; - - if (!isDefined(objectPermissionId)) { - throw new Error('Failed to upsert object permission'); } - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - 'rolesPermissions', - ]); - - return this.objectPermissionRepository.find({ - where: { - roleId: input.roleId, - objectMetadataId: In( - input.objectPermissions.map( - (objectPermission) => objectPermission.objectMetadataId, - ), - ), - }, - }); - } catch (error) { - await this.handleForeignKeyError({ - error, - roleId: input.roleId, - workspaceId, - objectMetadataIds: input.objectPermissions.map( - (objectPermission) => objectPermission.objectMetadataId, - ), - }); - - throw error; + if (objectMetadata.isSystem === true) { + throw new PermissionsException( + PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT, + PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT, + { + userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`, + }, + ); + } } + + const flatEntityToCreate: (UniversalFlatObjectPermission & { + id: string; + })[] = []; + const flatEntityToUpdate: UniversalFlatObjectPermission[] = []; + const flatEntityToDelete: UniversalFlatObjectPermission[] = []; + + const currentByObjectMetadataId = new Map( + currentObjectPermissionsForRole.map((op) => [op.objectMetadataId, op]), + ); + + for (const desired of input.objectPermissions) { + const current = currentByObjectMetadataId.get(desired.objectMetadataId); + + if (!isDefined(current)) { + flatEntityToCreate.push( + fromCreateObjectPermissionInputToUniversalFlatObjectPermission({ + createObjectPermissionInput: { + roleId: input.roleId, + objectMetadataId: desired.objectMetadataId, + canReadObjectRecords: desired.canReadObjectRecords, + canUpdateObjectRecords: desired.canUpdateObjectRecords, + canSoftDeleteObjectRecords: desired.canSoftDeleteObjectRecords, + canDestroyObjectRecords: desired.canDestroyObjectRecords, + }, + flatApplication, + flatRoleMaps, + flatObjectMetadataMaps, + }), + ); + } else { + const effectiveCanRead = + desired.canReadObjectRecords ?? current.canReadObjectRecords; + const effectiveCanUpdate = + desired.canUpdateObjectRecords ?? current.canUpdateObjectRecords; + const effectiveCanSoftDelete = + desired.canSoftDeleteObjectRecords ?? + current.canSoftDeleteObjectRecords; + const effectiveCanDestroy = + desired.canDestroyObjectRecords ?? current.canDestroyObjectRecords; + + const canChanged = + effectiveCanRead !== current.canReadObjectRecords || + effectiveCanUpdate !== current.canUpdateObjectRecords || + effectiveCanSoftDelete !== current.canSoftDeleteObjectRecords || + effectiveCanDestroy !== current.canDestroyObjectRecords; + + if (canChanged) { + const now = new Date().toISOString(); + flatEntityToUpdate.push({ + universalIdentifier: current.universalIdentifier, + applicationUniversalIdentifier: + current.applicationUniversalIdentifier, + roleUniversalIdentifier: current.roleUniversalIdentifier, + objectMetadataUniversalIdentifier: + current.objectMetadataUniversalIdentifier, + canReadObjectRecords: effectiveCanRead, + canUpdateObjectRecords: effectiveCanUpdate, + canSoftDeleteObjectRecords: effectiveCanSoftDelete, + canDestroyObjectRecords: effectiveCanDestroy, + createdAt: current.createdAt, + updatedAt: now, + }); + } + } + } + + for (const current of currentObjectPermissionsForRole) { + if (!desiredByObjectMetadataId.has(current.objectMetadataId)) { + flatEntityToDelete.push({ + universalIdentifier: current.universalIdentifier, + applicationUniversalIdentifier: + current.applicationUniversalIdentifier, + roleUniversalIdentifier: current.roleUniversalIdentifier, + objectMetadataUniversalIdentifier: + current.objectMetadataUniversalIdentifier, + canReadObjectRecords: current.canReadObjectRecords, + canUpdateObjectRecords: current.canUpdateObjectRecords, + canSoftDeleteObjectRecords: current.canSoftDeleteObjectRecords, + canDestroyObjectRecords: current.canDestroyObjectRecords, + createdAt: current.createdAt, + updatedAt: current.updatedAt, + }); + } + } + + if ( + flatEntityToCreate.length === 0 && + flatEntityToUpdate.length === 0 && + flatEntityToDelete.length === 0 + ) { + const unchanged = currentObjectPermissionsForRole.filter((op) => + desiredByObjectMetadataId.has(op.objectMetadataId), + ); + return unchanged; + } + + const buildAndRunResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + objectPermission: { + flatEntityToCreate, + flatEntityToUpdate, + flatEntityToDelete, + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: flatApplication.universalIdentifier, + }, + ); + + if (buildAndRunResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + buildAndRunResult, + 'Validation errors occurred while upserting object permissions', + ); + } + + const { flatObjectPermissionMaps: freshFlatObjectPermissionMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectPermissionMaps'], + }, + ); + + const resultObjectPermissions = Object.values( + freshFlatObjectPermissionMaps.byUniversalIdentifier, + ).filter( + (op): op is FlatObjectPermission => + isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier, + ); + + const desiredObjectMetadataIds = new Set( + input.objectPermissions.map((op) => op.objectMetadataId), + ); + const filtered = resultObjectPermissions.filter((op) => + desiredObjectMetadataIds.has(op.objectMetadataId), + ); + + return filtered; } - private async validateObjectPermissionsReadAndWriteConsistencyOrThrow({ + private validateObjectPermissionsReadAndWriteConsistencyOrThrow({ objectPermissions: newObjectPermissions, - roleWithObjectPermissions, + flatRole, + currentObjectPermissionsForRole, }: { objectPermissions: ObjectPermissionInput[]; - roleWithObjectPermissions: RoleEntity; - }) { - const existingObjectPermissions = - roleWithObjectPermissions.objectPermissions; - + flatRole: Pick< + FlatRole, + | 'canReadAllObjectRecords' + | 'canUpdateAllObjectRecords' + | 'canSoftDeleteAllObjectRecords' + | 'canDestroyAllObjectRecords' + >; + currentObjectPermissionsForRole: FlatObjectPermission[]; + }): void { for (const newObjectPermission of newObjectPermissions) { - const existingObjectRecordPermission = existingObjectPermissions.find( - (objectPermission) => - objectPermission.objectMetadataId === - newObjectPermission.objectMetadataId, - ); + const existingObjectRecordPermission = + currentObjectPermissionsForRole.find( + (objectPermission) => + objectPermission.objectMetadataId === + newObjectPermission.objectMetadataId, + ); const hasReadPermissionAfterUpdate = newObjectPermission.canReadObjectRecords ?? existingObjectRecordPermission?.canReadObjectRecords ?? - roleWithObjectPermissions.canReadAllObjectRecords; + flatRole.canReadAllObjectRecords; if (hasReadPermissionAfterUpdate === false) { const hasUpdatePermissionAfterUpdate = newObjectPermission.canUpdateObjectRecords ?? existingObjectRecordPermission?.canUpdateObjectRecords ?? - roleWithObjectPermissions.canUpdateAllObjectRecords; + flatRole.canUpdateAllObjectRecords; const hasSoftDeletePermissionAfterUpdate = newObjectPermission.canSoftDeleteObjectRecords ?? existingObjectRecordPermission?.canSoftDeleteObjectRecords ?? - roleWithObjectPermissions.canSoftDeleteAllObjectRecords; + flatRole.canSoftDeleteAllObjectRecords; const hasDestroyPermissionAfterUpdate = newObjectPermission.canDestroyObjectRecords ?? existingObjectRecordPermission?.canDestroyObjectRecords ?? - roleWithObjectPermissions.canDestroyAllObjectRecords; + flatRole.canDestroyAllObjectRecords; if ( hasUpdatePermissionAfterUpdate || @@ -194,91 +317,11 @@ export class ObjectPermissionService { } } - private async handleForeignKeyError({ - error, - roleId, - workspaceId, - objectMetadataIds, - }: { - error: Error; - roleId: string; - workspaceId: string; - objectMetadataIds: string[]; - }) { - if (error.message.includes('violates foreign key constraint')) { - const role = await this.roleRepository.findOne({ - where: { - id: roleId, - workspaceId, - }, - }); - - 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.`, - }, - ); - } - - const objectMetadata = await this.objectMetadataRepository.find({ - where: { - workspaceId, - id: In(objectMetadataIds), - }, - }); - - if (objectMetadata.length !== objectMetadataIds.length) { - throw new PermissionsException( - PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND, - PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, - { - userFriendlyMessage: msg`One or more objects you are trying to set permissions for could not be found. They may have been deleted.`, - }, - ); - } - } - } - - private async getRoleOrThrow({ - roleId, - workspaceId, - }: { - roleId: string; - workspaceId: string; - }) { - const role = await this.roleRepository.findOne({ - where: { - id: roleId, - workspaceId, - }, - relations: ['objectPermissions'], - }); - - 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.`, - }, + private async getFlatApplicationForWorkspace(workspaceId: string) { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, ); - } - - return role; - } - - private async validateRoleIsEditableOrThrow({ role }: { role: RoleEntity }) { - 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.`, - }, - ); - } + return workspaceCustomFlatApplication; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util.ts new file mode 100644 index 0000000000..0c232a793f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util.ts @@ -0,0 +1,12 @@ +import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type'; +import { type ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; + +export const fromFlatObjectPermissionToObjectPermissionDto = ( + flatObjectPermission: FlatObjectPermission, +): ObjectPermissionDTO => ({ + objectMetadataId: flatObjectPermission.objectMetadataId, + canReadObjectRecords: flatObjectPermission.canReadObjectRecords, + canUpdateObjectRecords: flatObjectPermission.canUpdateObjectRecords, + canSoftDeleteObjectRecords: flatObjectPermission.canSoftDeleteObjectRecords, + canDestroyObjectRecords: flatObjectPermission.canDestroyObjectRecords, +}); 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 8c301adce1..66f4649e54 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 @@ -39,6 +39,7 @@ import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object- import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; 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 { fromFlatObjectPermissionToObjectPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util'; 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'; @@ -211,10 +212,14 @@ export class RoleResolver { @Args('upsertObjectPermissionsInput') upsertObjectPermissionsInput: UpsertObjectPermissionsInput, ): Promise { - return this.objectPermissionService.upsertObjectPermissions({ - workspaceId: workspace.id, - input: upsertObjectPermissionsInput, - }); + const flatObjectPermissions = + await this.objectPermissionService.upsertObjectPermissions({ + workspaceId: workspace.id, + input: upsertObjectPermissionsInput, + }); + return flatObjectPermissions.map( + fromFlatObjectPermissionToObjectPermissionDto, + ); } @Mutation(() => [PermissionFlagDTO]) 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 6bf09b448b..9f03423a46 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 @@ -77,7 +77,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< }), this.objectPermissionRepository.find({ where: { workspaceId }, - select: ['id', 'roleId'], + select: ['id', 'universalIdentifier', 'roleId'], withDeleted: true, }), this.permissionFlagRepository.find({ diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 23bbaf17d3..922075d682 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -110,6 +110,7 @@ describe('WorkspaceEntityManager', () => { targetTableName: 'test_entity', fieldIds: ['field-id'], indexMetadataIds: [], + objectPermissionIds: [], viewIds: [], universalIdentifier: 'test-entity-id', description: null, @@ -125,6 +126,7 @@ describe('WorkspaceEntityManager', () => { updatedAt: new Date().toISOString(), applicationUniversalIdentifier: 'test-application-id', fieldUniversalIdentifiers: ['field-id'], + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: null, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts index 8d81cd2c61..7561e229d1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts @@ -224,6 +224,7 @@ describe('computeRelationConnectQueryConfigs', () => { isAuditLogged: true, isSearchable: true, universalIdentifier: partial.id, + objectPermissionIds: [], viewIds: [], applicationId: null, isLabelSyncedWithName: false, @@ -236,6 +237,13 @@ describe('computeRelationConnectQueryConfigs', () => { labelIdentifierFieldMetadataId: null, imageIdentifierFieldMetadataId: null, duplicateCriteria: null, + applicationUniversalIdentifier: '', + fieldUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], + viewUniversalIdentifiers: [], + indexMetadataUniversalIdentifiers: [], + labelIdentifierFieldMetadataUniversalIdentifier: null, + imageIdentifierFieldMetadataUniversalIdentifier: null, ...partial, }) as FlatObjectMetadata; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts index e5a196ad4c..cddb2decba 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts @@ -73,6 +73,7 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { universalIdentifier: objectMetadataId, fieldIds: ['name-id'], indexMetadataIds: [], + objectPermissionIds: [], viewIds: [], applicationId: null, } as unknown as FlatObjectMetadata; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts index 8374fba857..0a83e904f7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts @@ -26,6 +26,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => { workspaceId: 'test-workspace-id', universalIdentifier: 'test-object-id', indexMetadataIds: [], + objectPermissionIds: [], fieldIds, viewIds: [], applicationId: 'test-application-id', @@ -41,6 +42,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => { duplicateCriteria: null, applicationUniversalIdentifier: 'test-application-id', fieldUniversalIdentifiers: fieldIds, + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: null, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts index 9a5259fbbb..18178f0365 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts @@ -26,6 +26,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { workspaceId: 'test-workspace-id', universalIdentifier: 'test-object-id', indexMetadataIds: [], + objectPermissionIds: [], fieldIds, viewIds: [], applicationId: 'test-application-id', @@ -41,6 +42,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { duplicateCriteria: null, applicationUniversalIdentifier: 'test-application-id', fieldUniversalIdentifiers: fieldIds, + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: null, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts index dcd0b970d8..1c165bc3db 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/is-record-matching-rls-row-level-permission-predicate.util.spec.ts @@ -26,6 +26,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => { workspaceId: 'test-workspace-id', universalIdentifier: 'test-object-id', indexMetadataIds: [], + objectPermissionIds: [], fieldIds, viewIds: [], applicationId: 'test-application-id', @@ -41,6 +42,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => { duplicateCriteria: null, applicationUniversalIdentifier: 'test-application-id', fieldUniversalIdentifiers: fieldIds, + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier: null, 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 b017fac80a..02345256aa 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 @@ -1,11 +1,11 @@ import { - type ObjectsPermissionsByRoleId, type FeatureFlagKey, + type ObjectsPermissionsByRoleId, } from 'twenty-shared/types'; import { type EntityMetadata } from 'typeorm'; -import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type'; +import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type'; import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; @@ -33,6 +33,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { flatRoleMaps: 'flat-maps:role', flatRoleTargetMaps: 'flat-maps:role-target', flatPermissionFlagMaps: 'flat-maps:permission-flag', + flatObjectPermissionMaps: 'flat-maps:object-permission', 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/object-metadata/create-standard-object-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts index d86d5af895..d0345e7995 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts @@ -95,12 +95,14 @@ export const createStandardObjectFlatMetadata = < targetTableName: 'DEPRECATED', fieldIds: [], indexMetadataIds: [], + objectPermissionIds: [], viewIds: [], createdAt: now, updatedAt: now, id: standardObjectMetadataRelatedEntityIds[nameSingular].id, applicationUniversalIdentifier: twentyStandardApplicationId, fieldUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier, 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 41d9aca6da..c1e0f0eaea 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 @@ -1,8 +1,8 @@ import { v4 } from 'uuid'; import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; -import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant'; +import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; import { type AllStandardRoleName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-role-name.type'; import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type'; @@ -74,6 +74,7 @@ export const createStandardRoleFlatMetadata = ({ updatedAt: now, permissionFlagIds: [], permissionFlagUniversalIdentifiers: [], + objectPermissionUniversalIdentifiers: [], 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 810759e97a..69649bb176 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 @@ -168,6 +168,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ case 'frontComponent': case 'viewSort': case 'permissionFlag': + case 'objectPermission': 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 74dfe55103..fa7ed47963 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 @@ -20,6 +20,7 @@ import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engin 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 { WorkspaceMigrationObjectPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-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'; @@ -51,6 +52,7 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationViewGroupActionsBuilderService: WorkspaceMigrationViewGroupActionsBuilderService, private readonly workspaceMigrationViewFieldGroupActionsBuilderService: WorkspaceMigrationViewFieldGroupActionsBuilderService, private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService, + private readonly workspaceMigrationObjectPermissionActionsBuilderService: WorkspaceMigrationObjectPermissionActionsBuilderService, private readonly workspaceMigrationPermissionFlagActionsBuilderService: WorkspaceMigrationPermissionFlagActionsBuilderService, private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService, private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService, @@ -142,6 +144,7 @@ export class WorkspaceMigrationBuildOrchestratorService { flatRowLevelPermissionPredicateMaps, flatRowLevelPermissionPredicateGroupMaps, flatRoleMaps, + flatObjectPermissionMaps, flatPermissionFlagMaps, flatRoleTargetMaps, flatAgentMaps, @@ -484,6 +487,34 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatObjectPermissionMaps)) { + const { + from: fromFlatObjectPermissionMaps, + to: toFlatObjectPermissionMaps, + } = flatObjectPermissionMaps; + + const objectPermissionResult = + await this.workspaceMigrationObjectPermissionActionsBuilderService.validateAndBuild( + { + additionalCacheDataMaps, + from: fromFlatObjectPermissionMaps, + to: toFlatObjectPermissionMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, + workspaceId, + }, + ); + + if (objectPermissionResult.status === 'fail') { + orchestratorFailureReport.objectPermission.push( + ...objectPermissionResult.errors, + ); + } else { + orchestratorActionsReport.objectPermission = + objectPermissionResult.actions; + } + } + if (isDefined(flatPermissionFlagMaps)) { const { from: fromFlatPermissionFlagMaps, to: toFlatPermissionFlagMaps } = flatPermissionFlagMaps; @@ -839,6 +870,12 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.roleTarget.update, /// + // Object permissions + ...aggregatedOrchestratorActionsReport.objectPermission.delete, + ...aggregatedOrchestratorActionsReport.objectPermission.create, + ...aggregatedOrchestratorActionsReport.objectPermission.update, + /// + // Permission flags ...aggregatedOrchestratorActionsReport.permissionFlag.delete, ...aggregatedOrchestratorActionsReport.permissionFlag.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 1fc12fc5f6..4c4a4142c6 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 @@ -19,8 +19,10 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc "objectMetadata": [ "fieldUniversalIdentifiers", "indexMetadataUniversalIdentifiers", + "objectPermissionUniversalIdentifiers", "viewUniversalIdentifiers", ], + "objectPermission": [], "pageLayout": [ "tabUniversalIdentifiers", ], @@ -31,6 +33,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc "permissionFlag": [], "role": [ "roleTargetUniversalIdentifiers", + "objectPermissionUniversalIdentifiers", "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 074fa882cc..b7d0827895 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 @@ -38,6 +38,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = { commandMenuItem: {}, navigationMenuItem: {}, permissionFlag: {}, + objectPermission: {}, 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-object-permission.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type.ts new file mode 100644 index 0000000000..7a2b4211fb --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type.ts @@ -0,0 +1,7 @@ +import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type UniversalFlatObjectPermission = UniversalFlatEntityFrom< + ObjectPermissionEntity, + 'objectPermission' +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type.ts new file mode 100644 index 0000000000..0dfdebf9ef --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-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 FlatCreateObjectPermissionAction = + BaseFlatCreateWorkspaceMigrationAction<'objectPermission'>; + +export type FlatUpdateObjectPermissionAction = + BaseFlatUpdateWorkspaceMigrationAction<'objectPermission'>; + +export type FlatDeleteObjectPermissionAction = + BaseFlatDeleteWorkspaceMigrationAction<'objectPermission'>; + +export type UniversalCreateObjectPermissionAction = + BaseUniversalCreateWorkspaceMigrationAction<'objectPermission'>; + +export type UniversalUpdateObjectPermissionAction = + BaseUniversalUpdateWorkspaceMigrationAction<'objectPermission'>; + +export type UniversalDeleteObjectPermissionAction = + BaseUniversalDeleteWorkspaceMigrationAction<'objectPermission'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-actions-builder.service.ts new file mode 100644 index 0000000000..8758821e05 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-actions-builder.service.ts @@ -0,0 +1,121 @@ +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 { UniversalUpdateObjectPermissionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type'; +import { FlatObjectPermissionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-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 WorkspaceMigrationObjectPermissionActionsBuilderService extends WorkspaceEntityMigrationBuilderService< + typeof ALL_METADATA_NAME.objectPermission +> { + constructor( + private readonly flatObjectPermissionValidatorService: FlatObjectPermissionValidatorService, + ) { + super(ALL_METADATA_NAME.objectPermission); + } + + protected validateFlatEntityCreation( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.objectPermission + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.objectPermission, + 'create' + > { + const validationResult = + this.flatObjectPermissionValidatorService.validateFlatObjectPermissionCreation( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatObjectPermissionToValidate } = args; + + return { + status: 'success', + action: { + type: 'create', + metadataName: 'objectPermission', + flatEntity: flatObjectPermissionToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.objectPermission + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.objectPermission, + 'delete' + > { + const validationResult = + this.flatObjectPermissionValidatorService.validateFlatObjectPermissionDeletion( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatObjectPermissionToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete', + metadataName: 'objectPermission', + universalIdentifier: flatObjectPermissionToValidate.universalIdentifier, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.objectPermission + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.objectPermission, + 'update' + > { + const validationResult = + this.flatObjectPermissionValidatorService.validateFlatObjectPermissionUpdate( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { universalIdentifier, flatEntityUpdate } = args; + + const updateObjectPermissionAction: UniversalUpdateObjectPermissionAction = + { + type: 'update', + metadataName: 'objectPermission', + universalIdentifier, + update: flatEntityUpdate, + }; + + return { + status: 'success', + action: updateObjectPermissionAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-validator.service.ts new file mode 100644 index 0000000000..59067cb6e1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-validator.service.ts @@ -0,0 +1,252 @@ +import { Injectable } from '@nestjs/common'; + +import { msg, t } from '@lingui/core/macro'; +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 FlatObjectPermissionValidatorService { + validateFlatObjectPermissionCreation({ + flatEntityToValidate: flatObjectPermissionToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatObjectPermissionMaps: optimisticFlatObjectPermissionMaps, + flatRoleMaps, + flatObjectMetadataMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.objectPermission + >): FailedFlatEntityValidation<'objectPermission', 'create'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier: flatObjectPermissionToValidate.universalIdentifier, + roleUniversalIdentifier: + flatObjectPermissionToValidate.roleUniversalIdentifier, + objectMetadataUniversalIdentifier: + flatObjectPermissionToValidate.objectMetadataUniversalIdentifier, + }, + metadataName: 'objectPermission', + type: 'create', + }); + + const existingByUniversalId = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatObjectPermissionToValidate.universalIdentifier, + flatEntityMaps: optimisticFlatObjectPermissionMaps, + }); + + if (isDefined(existingByUniversalId)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Object permission with universal identifier ${flatObjectPermissionToValidate.universalIdentifier} already exists`, + userFriendlyMessage: msg`Object permission already exists`, + }); + } + + const referencedRole = findFlatEntityByUniversalIdentifier({ + universalIdentifier: + flatObjectPermissionToValidate.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 referencedObjectMetadata = findFlatEntityByUniversalIdentifier({ + universalIdentifier: + flatObjectPermissionToValidate.objectMetadataUniversalIdentifier, + flatEntityMaps: flatObjectMetadataMaps, + }); + + if (!isDefined(referencedObjectMetadata)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: t`Object metadata not found`, + userFriendlyMessage: msg`Object metadata not found`, + }); + } + + const duplicateForSameRoleAndObject = Object.values( + optimisticFlatObjectPermissionMaps.byUniversalIdentifier, + ).filter( + (op) => + isDefined(op) && + op.roleUniversalIdentifier === + flatObjectPermissionToValidate.roleUniversalIdentifier && + op.objectMetadataUniversalIdentifier === + flatObjectPermissionToValidate.objectMetadataUniversalIdentifier && + op.universalIdentifier !== + flatObjectPermissionToValidate.universalIdentifier, + ); + + if (duplicateForSameRoleAndObject.length > 0) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Object permission for this role and object already exists`, + userFriendlyMessage: msg`This object permission is already set for the role`, + }); + } + + return validationResult; + } + + validateFlatObjectPermissionUpdate({ + universalIdentifier, + flatEntityUpdate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatObjectPermissionMaps: optimisticFlatObjectPermissionMaps, + flatRoleMaps, + flatObjectMetadataMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.objectPermission + >): FailedFlatEntityValidation<'objectPermission', 'update'> { + const existingFlatObjectPermission = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatObjectPermissionMaps, + }); + + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'objectPermission', + type: 'update', + }); + + if (!isDefined(existingFlatObjectPermission)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND, + message: t`Object permission to update not found`, + userFriendlyMessage: msg`Object permission not found`, + }); + + return validationResult; + } + + const updatedFlatObjectPermission = { + ...existingFlatObjectPermission, + ...flatEntityUpdate, + }; + + const referencedRole = findFlatEntityByUniversalIdentifier({ + universalIdentifier: updatedFlatObjectPermission.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.objectMetadataUniversalIdentifier)) { + const referencedObjectMetadata = findFlatEntityByUniversalIdentifier({ + universalIdentifier: + updatedFlatObjectPermission.objectMetadataUniversalIdentifier, + flatEntityMaps: flatObjectMetadataMaps, + }); + + if (!isDefined(referencedObjectMetadata)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, + message: t`Object metadata not found`, + userFriendlyMessage: msg`Object metadata not found`, + }); + } + } + + const duplicateForSameRoleAndObject = Object.values( + optimisticFlatObjectPermissionMaps.byUniversalIdentifier, + ).filter( + (op) => + isDefined(op) && + op.roleUniversalIdentifier === + updatedFlatObjectPermission.roleUniversalIdentifier && + op.objectMetadataUniversalIdentifier === + updatedFlatObjectPermission.objectMetadataUniversalIdentifier && + op.universalIdentifier !== universalIdentifier, + ); + + if (duplicateForSameRoleAndObject.length > 0) { + validationResult.errors.push({ + code: PermissionsExceptionCode.INVALID_SETTING, + message: t`Object permission for this role and object already exists`, + userFriendlyMessage: msg`This object permission is already set for the role`, + }); + } + + return validationResult; + } + + validateFlatObjectPermissionDeletion({ + flatEntityToValidate: { universalIdentifier }, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatObjectPermissionMaps: optimisticFlatObjectPermissionMaps, + flatRoleMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.objectPermission + >): FailedFlatEntityValidation<'objectPermission', 'delete'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'objectPermission', + type: 'delete', + }); + + const existingFlatObjectPermission = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatObjectPermissionMaps, + }); + + if (!isDefined(existingFlatObjectPermission)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND, + message: t`Object permission to delete not found`, + userFriendlyMessage: msg`Object permission not found`, + }); + } else { + const referencedRole = findFlatEntityByUniversalIdentifier({ + universalIdentifier: + existingFlatObjectPermission.roleUniversalIdentifier, + flatEntityMaps: flatRoleMaps, + }); + + if (isDefined(referencedRole) && !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 25754c79a6..411ca97a16 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 @@ -11,6 +11,7 @@ import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspac 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 { FlatObjectPermissionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-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'; @@ -39,6 +40,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp FlatViewGroupValidatorService, FlatViewFieldGroupValidatorService, FlatViewSortValidatorService, + FlatObjectPermissionValidatorService, FlatPermissionFlagValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, @@ -68,6 +70,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp FlatViewGroupValidatorService, FlatViewFieldGroupValidatorService, FlatViewSortValidatorService, + FlatObjectPermissionValidatorService, FlatPermissionFlagValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, 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 d9276bdf54..692256e533 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 @@ -9,6 +9,7 @@ import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engin 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 { WorkspaceMigrationObjectPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-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'; @@ -41,6 +42,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationViewGroupActionsBuilderService, WorkspaceMigrationViewFieldGroupActionsBuilderService, WorkspaceMigrationViewSortActionsBuilderService, + WorkspaceMigrationObjectPermissionActionsBuilderService, WorkspaceMigrationPermissionFlagActionsBuilderService, WorkspaceMigrationIndexActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, @@ -69,6 +71,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationViewGroupActionsBuilderService, WorkspaceMigrationViewFieldGroupActionsBuilderService, WorkspaceMigrationViewSortActionsBuilderService, + WorkspaceMigrationObjectPermissionActionsBuilderService, WorkspaceMigrationPermissionFlagActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/create-object-permission-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/create-object-permission-action-handler.service.ts new file mode 100644 index 0000000000..31e2a1e2a7 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/create-object-permission-action-handler.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; + +import { v4 } from 'uuid'; + +import { + FlatCreateObjectPermissionAction, + UniversalCreateObjectPermissionAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type'; +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; +import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; + +@Injectable() +export class CreateObjectPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create', + 'objectPermission', +) { + override async transpileUniversalActionToFlatAction({ + action, + allFlatEntityMaps, + flatApplication, + workspaceId, + }: WorkspaceMigrationActionRunnerArgs): Promise { + const { roleId, objectMetadataId } = + resolveUniversalRelationIdentifiersToIds({ + flatEntityMaps: allFlatEntityMaps, + metadataName: action.metadataName, + universalForeignKeyValues: action.flatEntity, + }); + + return { + ...action, + flatEntity: { + ...action.flatEntity, + roleId, + objectMetadataId, + 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/object-permission/services/delete-object-permission-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/delete-object-permission-action-handler.service.ts new file mode 100644 index 0000000000..ec72b88b2f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/delete-object-permission-action-handler.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; + +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { + FlatDeleteObjectPermissionAction, + UniversalDeleteObjectPermissionAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type'; +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteObjectPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete', + 'objectPermission', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return this.transpileUniversalDeleteActionToFlatDeleteAction(context); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + + const objectPermissionRepository = + queryRunner.manager.getRepository( + ObjectPermissionEntity, + ); + + await objectPermissionRepository.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/object-permission/services/update-object-permission-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/update-object-permission-action-handler.service.ts new file mode 100644 index 0000000000..148d7330a8 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/update-object-permission-action-handler.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; + +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { + FlatUpdateObjectPermissionAction, + UniversalUpdateObjectPermissionAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type'; +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; +import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util'; + +@Injectable() +export class UpdateObjectPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update', + 'objectPermission', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, allFlatEntityMaps } = context; + + const flatObjectPermission = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: allFlatEntityMaps.flatObjectPermissionMaps, + universalIdentifier: action.universalIdentifier, + }); + + const update = resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'objectPermission', + universalUpdate: action.update, + allFlatEntityMaps, + }); + + return { + type: 'update', + metadataName: 'objectPermission', + entityId: flatObjectPermission.id, + update, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + const { entityId, update } = flatAction; + + const objectPermissionRepository = + queryRunner.manager.getRepository( + ObjectPermissionEntity, + ); + + await objectPermissionRepository.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/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts index 3222f9fc59..921773d7a0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util.ts @@ -122,6 +122,7 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({ fieldIds: [], viewIds: [], indexMetadataIds: [], + objectPermissionIds: [], ...emptyUniversalForeignKeyAggregators, }; }; 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 037241e014..907ad2b816 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 @@ -24,6 +24,9 @@ import { UpdateLogicFunctionActionHandlerService } from 'src/engine/workspace-ma 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 { CreateObjectPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/create-object-permission-action-handler.service'; +import { DeleteObjectPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/delete-object-permission-action-handler.service'; +import { UpdateObjectPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/update-object-permission-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'; @@ -141,6 +144,10 @@ import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/ UpdatePermissionFlagActionHandlerService, DeletePermissionFlagActionHandlerService, + CreateObjectPermissionActionHandlerService, + UpdateObjectPermissionActionHandlerService, + DeleteObjectPermissionActionHandlerService, + 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 48f3440fee..9a67e3956a 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 @@ -20,6 +20,7 @@ export const METADATA_EVENTS_TO_EMIT = { commandMenuItem: true, navigationMenuItem: true, permissionFlag: true, + objectPermission: 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 1fbb1d8f06..b0dfe4be6d 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 @@ -90,6 +90,7 @@ const deriveAllMetadataEventsFromCreateAction = ( case 'frontComponent': case 'navigationMenuItem': case 'permissionFlag': + case 'objectPermission': 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 ccc14d28f7..6ae27f22ce 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 @@ -54,6 +54,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({ case 'frontComponent': case 'navigationMenuItem': case 'permissionFlag': + case 'objectPermission': 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 6be03a06d6..20bea00184 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 @@ -132,6 +132,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({ case 'frontComponent': case 'navigationMenuItem': case 'permissionFlag': + case 'objectPermission': 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 a67b92480f..8c09237c40 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 @@ -69,6 +69,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({ case 'frontComponent': case 'navigationMenuItem': case 'permissionFlag': + case 'objectPermission': 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 6ca5f87ae2..8a8d569d53 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 @@ -41,6 +41,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({ case 'frontComponent': case 'navigationMenuItem': case 'permissionFlag': + case 'objectPermission': 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 e3c7fd435f..af3aeb4b88 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 @@ -6,8 +6,8 @@ import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } f import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { type AllFlatWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util'; +import { type AllFlatWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; export type OptimisticallyApplyUpdateActionOnAllFlatEntityMapsArgs = { flatAction: AllFlatWorkspaceMigrationAction<'update'>; @@ -62,6 +62,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({ case 'frontComponent': case 'navigationMenuItem': case 'permissionFlag': + case 'objectPermission': 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 4e2bcc2f02..eee296ecf9 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 @@ -493,12 +493,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 objectPermissionErrors = + res.body.errors[0].extensions.errors?.objectPermission ?? []; + const hasRoleNotEditable = objectPermissionErrors.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/object-permission/__snapshots__/failing-object-permission-upsert.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-permission/__snapshots__/failing-object-permission-upsert.integration-spec.ts.snap new file mode 100644 index 0000000000..46bdc11913 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-permission/__snapshots__/failing-object-permission-upsert.integration-spec.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Object permission upsert should fail when object is system object 1`] = ` +{ + "extensions": { + "code": "FORBIDDEN", + "subCode": "CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT", + "userFriendlyMessage": "You cannot set permissions on system objects as they are managed by the platform.", + }, + "message": "Cannot add object permission on system object", + "name": "ForbiddenError", +} +`; + +exports[`Object permission upsert should fail when objectMetadataId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "OBJECT_METADATA_NOT_FOUND_PERMISSIONS", + "userFriendlyMessage": "The object you are trying to set permissions for could not be found. It may have been deleted.", + }, + "message": "Object metadata id not found", + "name": "NotFoundError", +} +`; + +exports[`Object permission upsert should fail when read=false but canUpdateObjectRecords=true (read/write consistency) 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "subCode": "CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + "message": "Cannot give update permission to non-readable object", + "name": "UserInputError", +} +`; + +exports[`Object permission upsert should fail when role is not editable (system role) 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "objectPermission": [ + { + "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": { + "objectMetadataUniversalIdentifier": Any, + "roleUniversalIdentifier": Any, + "universalIdentifier": Any, + }, + "metadataName": "objectPermission", + "status": "fail", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 objectPermission", + "summary": { + "objectPermission": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while upserting object permissions", + "name": "GraphQLError", +} +`; + +exports[`Object permission 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[`Object permission 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/object-permission/failing-object-permission-upsert.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-permission/failing-object-permission-upsert.integration-spec.ts new file mode 100644 index 0000000000..d5d79195df --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-permission/failing-object-permission-upsert.integration-spec.ts @@ -0,0 +1,275 @@ +import gql from 'graphql-tag'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { upsertObjectPermissions } from 'test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.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 { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; + +type GlobalTestContext = { + editableRoleId: string; + nonEditableRoleId: string; + systemObjectMetadataId: string; + nonSystemObjectMetadataId: string; + editableRoleWithNoReadId: string; +}; + +type TestContext = { + input: (globalContext: GlobalTestContext) => UpsertObjectPermissionsInput; +}; + +const failingObjectPermissionUpsertTestCases: EachTestingContext[] = + [ + { + title: 'when roleId is not a valid UUID', + context: { + input: () => ({ + roleId: 'invalid-uuid', + objectPermissions: [ + { + objectMetadataId: v4(), + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }), + }, + }, + { + title: 'when roleId does not exist', + context: { + input: () => ({ + roleId: v4(), + objectPermissions: [ + { + objectMetadataId: v4(), + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }), + }, + }, + { + title: 'when role is not editable (system role)', + context: { + input: (globalContext) => ({ + roleId: globalContext.nonEditableRoleId, + objectPermissions: [ + { + objectMetadataId: globalContext.nonSystemObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }), + }, + }, + { + title: 'when objectMetadataId does not exist', + context: { + input: (globalContext) => ({ + roleId: globalContext.editableRoleId, + objectPermissions: [ + { + objectMetadataId: v4(), + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }), + }, + }, + { + title: 'when object is system object', + context: { + input: (globalContext) => ({ + roleId: globalContext.editableRoleId, + objectPermissions: [ + { + objectMetadataId: globalContext.systemObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }), + }, + }, + { + title: 'when read=false but canUpdateObjectRecords=true (read/write consistency)', + context: { + input: (globalContext) => ({ + roleId: globalContext.editableRoleWithNoReadId, + objectPermissions: [ + { + objectMetadataId: globalContext.nonSystemObjectMetadataId, + canReadObjectRecords: false, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }), + }, + }, + ]; + +describe('Object permission upsert should fail', () => { + let editableRoleId: string; + let nonEditableRoleId: string; + let systemObjectMetadataId: string; + let nonSystemObjectMetadataId: string; + let editableRoleWithNoReadId: string; + + beforeAll(async () => { + const { data: roleData } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Role For Object Permission Failing', + description: 'Role for object permission 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: roleNoReadData } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Role No Read For Object Permission', + description: 'Role with no read for consistency test', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + editableRoleWithNoReadId = roleNoReadData?.createOneRole?.id; + jestExpectToBeDefined(editableRoleWithNoReadId); + + 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; + + const getObjectMetadataOperation = { + query: gql` + query { + objects(paging: { first: 1000 }) { + edges { + node { + id + nameSingular + isSystem + } + } + } + } + `, + }; + + const objectMetadataResponse = await makeMetadataAPIRequest( + getObjectMetadataOperation, + ); + const edges = objectMetadataResponse.body.data?.objects?.edges ?? []; + const systemObjectNode = edges.find( + (edge: { node: { isSystem: boolean | string } }) => + edge.node.isSystem === true || String(edge.node.isSystem) === 'true', + )?.node; + jestExpectToBeDefined(systemObjectNode); + systemObjectMetadataId = systemObjectNode.id; + + const nonSystemObjectNode = edges.find( + (edge: { node: { isSystem: boolean | string } }) => + edge.node.isSystem === false || String(edge.node.isSystem) === 'false', + )?.node; + jestExpectToBeDefined(nonSystemObjectNode); + nonSystemObjectMetadataId = nonSystemObjectNode.id; + }); + + afterAll(async () => { + if (isDefined(editableRoleId)) { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: editableRoleId }, + }); + } + if (isDefined(editableRoleWithNoReadId)) { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: editableRoleWithNoReadId }, + }); + } + }); + + it.each(eachTestingContextFilter(failingObjectPermissionUpsertTestCases))( + '$title', + async ({ context }) => { + const globalContext: GlobalTestContext = { + editableRoleId: editableRoleId ?? '', + nonEditableRoleId: nonEditableRoleId ?? '', + systemObjectMetadataId: systemObjectMetadataId ?? '', + nonSystemObjectMetadataId: nonSystemObjectMetadataId ?? '', + editableRoleWithNoReadId: editableRoleWithNoReadId ?? '', + }; + const input = context.input(globalContext); + + const { errors } = await upsertObjectPermissions({ + expectToFail: true, + input, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-permission/successful-object-permission-upsert.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-permission/successful-object-permission-upsert.integration-spec.ts new file mode 100644 index 0000000000..a02793584e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-permission/successful-object-permission-upsert.integration-spec.ts @@ -0,0 +1,146 @@ +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { upsertObjectPermissions } from 'test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.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 { isDefined } from 'twenty-shared/utils'; + +describe('Object permission upsert should succeed', () => { + let createdRoleId: string; + let customObjectMetadataId: string; + + beforeAll(async () => { + const { data: roleData } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Role For Object Permission Success', + description: 'Role for object permission successful tests', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + createdRoleId = roleData?.createOneRole?.id; + jestExpectToBeDefined(createdRoleId); + + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'testObjectPermissionSuccess', + namePlural: 'testObjectPermissionSuccesses', + labelSingular: 'Test Object Permission Success', + labelPlural: 'Test Object Permission Successes', + icon: 'IconSettings', + }, + }); + + customObjectMetadataId = createOneObject.id; + jestExpectToBeDefined(customObjectMetadataId); + }); + + afterAll(async () => { + if (isDefined(createdRoleId)) { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: createdRoleId }, + }); + } + if (isDefined(customObjectMetadataId)) { + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: customObjectMetadataId }, + }); + } + }); + + it('should upsert one object permission (create)', async () => { + const { data } = await upsertObjectPermissions({ + expectToFail: false, + input: { + roleId: createdRoleId, + objectPermissions: [ + { + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + }, + }); + + expect(data?.upsertObjectPermissions).toHaveLength(1); + expect(data?.upsertObjectPermissions?.[0]).toMatchObject({ + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }); + }); + + it('should upsert to update existing object permission flags', async () => { + const { data } = await upsertObjectPermissions({ + expectToFail: false, + input: { + roleId: createdRoleId, + objectPermissions: [ + { + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: false, + }, + ], + }, + }); + + expect(data?.upsertObjectPermissions).toHaveLength(1); + expect(data?.upsertObjectPermissions?.[0]).toMatchObject({ + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: false, + }); + }); + + it('should upsert with read and all write permissions true', async () => { + const { data } = await upsertObjectPermissions({ + expectToFail: false, + input: { + roleId: createdRoleId, + objectPermissions: [ + { + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + }, + ], + }, + }); + + expect(data?.upsertObjectPermissions).toHaveLength(1); + expect(data?.upsertObjectPermissions?.[0]).toMatchObject({ + objectMetadataId: customObjectMetadataId, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions-query-factory.util.ts new file mode 100644 index 0000000000..f20535ae64 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions-query-factory.util.ts @@ -0,0 +1,34 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; + +export type UpsertObjectPermissionsFactoryInput = UpsertObjectPermissionsInput; + +const DEFAULT_OBJECT_PERMISSION_GQL_FIELDS = ` + objectMetadataId + canReadObjectRecords + canUpdateObjectRecords + canSoftDeleteObjectRecords + canDestroyObjectRecords +`; + +export const upsertObjectPermissionsQueryFactory = ({ + input, + gqlFields = DEFAULT_OBJECT_PERMISSION_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpsertObjectPermissions( + $upsertObjectPermissionsInput: UpsertObjectPermissionsInput! + ) { + upsertObjectPermissions( + upsertObjectPermissionsInput: $upsertObjectPermissionsInput + ) { + ${gqlFields} + } + } + `, + variables: { + upsertObjectPermissionsInput: input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.util.ts new file mode 100644 index 0000000000..a4bff09d05 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.util.ts @@ -0,0 +1,44 @@ +import { + type UpsertObjectPermissionsFactoryInput, + upsertObjectPermissionsQueryFactory, +} from 'test/integration/metadata/suites/object-permission/utils/upsert-object-permissions-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 ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; + +export const upsertObjectPermissions = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + upsertObjectPermissions: ObjectPermissionDTO[]; +}> => { + const graphqlOperation = upsertObjectPermissionsQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Object permissions upsert should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Object permissions 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 7b9db86529..36dff91515 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 @@ -22,6 +22,7 @@ export const ALL_METADATA_NAME = { commandMenuItem: 'commandMenuItem', navigationMenuItem: 'navigationMenuItem', permissionFlag: 'permissionFlag', + objectPermission: 'objectPermission', frontComponent: 'frontComponent', webhook: 'webhook', } as const;