From ea3c5d2d4540e6aa0605a23a1f3c24c4ec721085 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:06:11 +0100 Subject: [PATCH] Migrate role and role target to v2 (#16009) # Introduction close https://github.com/twentyhq/core-team-issues/issues/1930 close https://github.com/twentyhq/core-team-issues/issues/1929 Migrating role and roleTarget entities to the v2 core engine, allowing v2 caching leverage and allow migrating agent to v2 that needs role target in prior After agent we should be able to pass twenty standard app totally though workspace migration ## Role target assignation Please note that role target have 3 creation entrypoints: - Agent - User workspace - ApiKey Refactored all 3 of them to pass through a new role-target.service.ts that consumes the v2 under the hood. --------- Co-authored-by: Weiko --- .../hooks/useMetadataErrorHandler.ts | 2 + .../1763896975223-syncable-role-target.ts | 35 ++ ...3-update-role-targets-unique-constraint.ts | 37 ++ .../api-key/api-key-role.service.spec.ts | 179 ++----- .../api-key/api-key-role.service.ts | 60 +-- .../core-modules/api-key/api-key.module.ts | 6 +- .../api-key/api-key.service.spec.ts | 101 ++-- .../core-modules/api-key/api-key.service.ts | 40 +- .../application/application-sync.module.ts | 6 +- .../application/application.resolver.ts | 4 +- .../user-workspace.service.spec.ts | 41 +- .../user-workspace/user-workspace.service.ts | 41 +- .../services/workspace-invitation.service.ts | 16 +- .../workspace/workspace.resolver.ts | 7 +- .../ai-agent-role/agent-role.service.spec.ts | 119 ++--- .../ai/ai-agent-role/ai-agent-role.module.ts | 2 + .../ai/ai-agent-role/ai-agent-role.service.ts | 37 +- .../ai/ai-agent/ai-agent.module.ts | 4 +- ...rties-to-compare-and-stringify.constant.ts | 10 + ...metadata-many-to-one-relations.constant.ts | 7 + .../constant/all-metadata-name.constant.ts | 2 + ...quired-metadata-for-validation.constant.ts | 4 + .../all-flat-entity-types-by-metadata-name.ts | 32 ++ ...ole-target-editable-properties.constant.ts | 7 + ...-target-foreign-key-properties.constant.ts | 7 + ...pace-flat-role-target-map-cache.service.ts | 55 ++ .../types/flat-role-target-maps.type.ts | 4 + .../types/flat-role-target.type.ts | 12 + ...role-target-foreign-key-properties.type.ts | 4 + ...-flat-role-target-from-foreign-key.util.ts | 23 + ...-target-entity-to-flat-role-target.util.ts | 19 + .../flat-role-editable-properties.constant.ts | 16 + ...flat-role-required-properties.constants.ts | 11 + .../flat-role/types/flat-role.type.ts | 31 +- ...-role-input-to-flat-role-to-create.util.ts | 58 +++ ...e-role-input-to-flat-role-or-throw.util.ts | 35 ++ .../from-role-entity-to-flat-role.util.ts | 8 +- ...ndard-role-definition-to-flat-role.util.ts | 8 + ...ut-to-flat-role-to-update-or-throw.util.ts | 39 ++ .../permissions/permissions.module.ts | 4 +- .../role-target/role-target.module.ts | 18 + .../services/role-target.service.ts | 173 ++++++ .../types/create-role-target.input.ts | 9 + ...nput-to-flat-role-target-to-create.util.ts | 50 ++ .../metadata-modules/role/dtos/role.dto.ts | 6 +- .../role/exceptions/role-target.exception.ts | 16 + .../role/role-targets.entity.ts | 18 +- .../metadata-modules/role/role.entity.ts | 10 +- .../metadata-modules/role/role.module.ts | 41 +- .../metadata-modules/role/role.resolver.ts | 35 +- .../metadata-modules/role/role.service.ts | 491 ++++++++---------- .../workspace-flat-role-map-cache.service.ts | 128 +++++ .../role/utils/fromFlatRoleToRoleDto.util.ts | 38 ++ .../utils/fromRoleEntityToRoleDto.util.ts | 9 +- .../user-role/user-role.module.ts | 2 + .../user-role/user-role.service.ts | 80 +-- .../workspace-permissions-cache.service.ts | 18 +- .../dev-seeder-permissions.service.ts | 44 +- .../dev-seeder/dev-seeder.module.ts | 2 + ...metadata-validation-response-error.util.ts | 2 + ...ilder-graphql-api-exception.interceptor.ts | 38 ++ ...ce-migration-build-orchestrator.service.ts | 83 ++- ...ce-migration-role-target-action-v2.type.ts | 26 + ...-v2-role-target-actions-builder.service.ts | 105 ++++ ...workspace-migration-role-action-v2.type.ts | 26 + ...gration-v2-role-actions-builder.service.ts | 96 ++++ .../flat-role-target-validator.service.ts | 179 +++++++ .../services/flat-role-validator.service.ts | 176 +++++++ ...le-target-assignation-availability.util.ts | 48 ++ ...ole-target-targets-only-one-entity.util.ts | 30 ++ .../utils/validate-role-is-editable.util.ts | 26 + .../validate-role-label-uniqueness.util.ts | 28 + ...read-write-permissions-consistency.util.ts | 32 ++ ...le-required-properties-are-defined.util.ts | 28 + ...ace-migration-builder-validators.module.ts | 6 + .../workspace-migration-builder-v2.module.ts | 6 + ...eate-role-target-action-handler.service.ts | 55 ++ ...lete-role-target-action-handler.service.ts | 61 +++ ...date-role-target-action-handler.service.ts | 70 +++ .../create-role-action-handler.service.ts | 59 +++ .../delete-role-action-handler.service.ts | 56 ++ .../update-role-action-handler.service.ts | 67 +++ ...migration-runner-action-handlers.module.ts | 14 + .../workspace-migration-runner-v2.service.ts | 42 +- .../factories/standard-role.factory.ts | 6 +- .../services/workspace-sync-role.service.ts | 9 +- .../standard-role-definition.interface.ts | 11 +- .../suites/role/role.integration-spec.ts | 325 ------------ ...r-settings-permissions.integration-spec.ts | 167 +++--- .../roles.integration-spec.ts | 28 - .../view/destroy-view.integration-spec.ts | 2 +- ...ny-view-fields-v2.integration-spec.ts.snap | 4 + ...ate-view-field-v2.integration-spec.ts.snap | 12 + ...ect-on-view-field.integration-spec.ts.snap | 20 + ...ny-view-groups-v2.integration-spec.ts.snap | 4 + ...ne-field-metadata.integration-spec.ts.snap | 4 + ...ne-field-metadata.integration-spec.ts.snap | 12 + ...ne-field-metadata.integration-spec.ts.snap | 8 + ...-metadata-enum-v2.integration-spec.ts.snap | 280 ++++++++++ ...field-metadata-v2.integration-spec.ts.snap | 272 ++++++++++ ...morph-relation-v2.integration-spec.ts.snap | 8 + ...morph-relation-v2.integration-spec.ts.snap | 68 +++ ...relation-creation.integration-spec.ts.snap | 96 ++++ ...a-relation-update.integration-spec.ts.snap | 4 + ...ue-field-metadata.integration-spec.ts.snap | 8 + ...bject-metadata-v2.integration-spec.ts.snap | 68 +++ ...e-object-metadata.integration-spec.ts.snap | 12 + ...g-delete-one-role.integration-spec.ts.snap | 42 ++ ...ing-role-creation.integration-spec.ts.snap | 310 +++++++++++ ...iling-role-update.integration-spec.ts.snap | 419 +++++++++++++++ ...ailing-delete-one-role.integration-spec.ts | 91 ++++ .../failing-role-creation.integration-spec.ts | 161 ++++++ .../failing-role-update.integration-spec.ts | 271 ++++++++++ ...essful-delete-one-role.integration-spec.ts | 81 +++ ...ccessful-role-creation.integration-spec.ts | 112 ++++ ...successful-role-update.integration-spec.ts | 261 ++++++++++ ...-workspace-member-role.integration-spec.ts | 110 ++++ .../create-one-role-query-factory.util.ts | 39 ++ .../suites/role/utils/create-one-role.util.ts | 43 ++ .../delete-one-role-query-factory.util.ts | 19 + .../suites/role/utils/delete-one-role.util.ts | 41 ++ .../role/utils/find-one-role-by-label.util.ts | 33 ++ .../utils/find-roles-query-factory.util.ts | 33 ++ .../suites/role/utils/find-roles.util.ts | 41 ++ .../update-one-role-query-factory.util.ts | 43 ++ .../suites/role/utils/update-one-role.util.ts | 42 ++ ...orkspace-member-role-query-factory.util.ts | 43 ++ .../update-workspace-member-role.util.ts | 48 ++ .../view-group.integration-spec.ts.snap | 4 + .../suites/view-group.integration-spec.ts | 4 +- .../metadata/all-metadata-name.constant.ts | 2 + 131 files changed, 6082 insertions(+), 1319 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1763896975223-syncable-role-target.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1764329720503-update-role-targets-unique-constraint.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role-target/role-target.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role-target/services/role-target.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role-target/types/create-role-target.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role-target/utils/from-create-role-target-input-to-flat-role-target-to-create.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role/exceptions/role-target.exception.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service.ts delete mode 100644 packages/twenty-server/test/integration/graphql/suites/role/role.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-delete-one-role.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-creation.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-update.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/failing-delete-one-role.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/failing-role-creation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/failing-role-update.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/successful-delete-one-role.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/successful-role-creation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/successful-role-update.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/successful-update-workspace-member-role.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/find-one-role-by-label.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role.util.ts 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 4ed468173a..5058e453fc 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 @@ -23,6 +23,8 @@ export const useMetadataErrorHandler = () => { cronTrigger: t`cron trigger`, databaseEventTrigger: t`database trigger`, routeTrigger: t`route trigger`, + role: t`role`, + roleTarget: t`role target`, } as const satisfies Record; const handleMetadataError = useCallback( diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1763896975223-syncable-role-target.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1763896975223-syncable-role-target.ts new file mode 100644 index 0000000000..3acb23f553 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1763896975223-syncable-role-target.ts @@ -0,0 +1,35 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class SyncableRoleTarget1763896975223 implements MigrationInterface { + name = 'SyncableRoleTarget1763896975223'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD "universalIdentifier" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD "applicationId" uuid`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_3e571e80f99488686015f3d00c" ON "core"."roleTargets" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "FK_d4fcfdc3cd562a3e81fa9f0dae5" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "FK_d4fcfdc3cd562a3e81fa9f0dae5"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_3e571e80f99488686015f3d00c"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP COLUMN "applicationId"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP COLUMN "universalIdentifier"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1764329720503-update-role-targets-unique-constraint.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1764329720503-update-role-targets-unique-constraint.ts new file mode 100644 index 0000000000..ac1e5a4fb8 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1764329720503-update-role-targets-unique-constraint.ts @@ -0,0 +1,37 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class UpdateRoleTargetsUniqueConstraint1764329720503 + implements MigrationInterface +{ + name = 'UpdateRoleTargetsUniqueConstraint1764329720503'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_API_KEY" UNIQUE ("workspaceId", "apiKeyId")`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_AGENT" UNIQUE ("workspaceId", "agentId")`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_USER_WORKSPACE" UNIQUE ("workspaceId", "userWorkspaceId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_USER_WORKSPACE"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_AGENT"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_API_KEY"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE" UNIQUE ("workspaceId", "userWorkspaceId", "agentId", "apiKeyId")`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts index 15418f067f..b7fa79bde5 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts @@ -9,6 +9,7 @@ import { ApiKeyExceptionCode, } from 'src/engine/core-modules/api-key/api-key.exception'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @@ -23,6 +24,7 @@ describe('ApiKeyRoleService', () => { let mockApiKeyRepository: any; let mockDataSource: any; let mockWorkspacePermissionsCacheService: any; + let mockRoleTargetService: any; const mockWorkspaceId = 'workspace-123'; const mockApiKeyId = 'api-key-456'; @@ -77,12 +79,6 @@ describe('ApiKeyRoleService', () => { apiKey: mockApiKey, } as RoleTargetsEntity; - const mockNewRoleTarget = { - ...mockRoleTarget, - id: 'role-target-456', - roleId: mockNewRoleId, - } as RoleTargetsEntity; - beforeEach(async () => { mockRoleTargetsRepository = { save: jest.fn(), @@ -112,6 +108,11 @@ describe('ApiKeyRoleService', () => { getApiKeyRoleMapFromCache: jest.fn(), }; + mockRoleTargetService = { + create: jest.fn(), + delete: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ providers: [ ApiKeyRoleService, @@ -139,6 +140,10 @@ describe('ApiKeyRoleService', () => { provide: WorkspacePermissionsCacheService, useValue: mockWorkspacePermissionsCacheService, }, + { + provide: RoleTargetService, + useValue: mockRoleTargetService, + }, ], }).compile(); @@ -153,84 +158,12 @@ describe('ApiKeyRoleService', () => { expect(service).toBeDefined(); }); - describe('assignRoleToApiKeyWithManager', () => { - it('should assign role using provided transaction manager', async () => { - const mockManagerDelete = jest.fn().mockResolvedValue({ affected: 1 }); - const mockManagerCreate = jest.fn().mockReturnValue(mockNewRoleTarget); - const mockManagerSave = jest.fn().mockResolvedValue(mockNewRoleTarget); - - const mockManager = { - delete: mockManagerDelete, - create: mockManagerCreate, - save: mockManagerSave, - }; - - await service.assignRoleToApiKeyWithManager(mockManager as any, { - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }); - - expect(mockManagerDelete).toHaveBeenCalledWith(RoleTargetsEntity, { - apiKeyId: mockApiKeyId, - workspaceId: mockWorkspaceId, - }); - expect(mockManagerCreate).toHaveBeenCalledWith(RoleTargetsEntity, { - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }); - expect(mockManagerSave).toHaveBeenCalledWith(mockNewRoleTarget); - }); - - it('should handle manager operation failures', async () => { - const mockManagerDelete = jest - .fn() - .mockRejectedValue(new Error('Delete failed')); - const mockManagerCreate = jest.fn(); - const mockManagerSave = jest.fn(); - - const mockManager = { - delete: mockManagerDelete, - create: mockManagerCreate, - save: mockManagerSave, - }; - - await expect( - service.assignRoleToApiKeyWithManager(mockManager as any, { - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }), - ).rejects.toThrow('Delete failed'); - - expect(mockManagerDelete).toHaveBeenCalled(); - expect(mockManagerCreate).not.toHaveBeenCalled(); - expect(mockManagerSave).not.toHaveBeenCalled(); - }); - }); - describe('assignRoleToApiKey', () => { - it('should assign a new role to API key using transaction', async () => { + it('should assign a new role to API key using roleTargetService', async () => { mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); mockRoleRepository.findOne.mockResolvedValue(mockNewRole); mockRoleTargetsRepository.findOne.mockResolvedValue(null); - - const mockManagerDelete = jest.fn().mockResolvedValue({ affected: 1 }); - const mockManagerCreate = jest.fn().mockReturnValue(mockNewRoleTarget); - const mockManagerSave = jest.fn().mockResolvedValue(mockNewRoleTarget); - - mockDataSource.transaction.mockImplementation( - async (callback: (manager: any) => Promise) => { - const mockManager = { - delete: mockManagerDelete, - create: mockManagerCreate, - save: mockManagerSave, - }; - - return await callback(mockManager); - }, - ); + mockRoleTargetService.create.mockResolvedValue(undefined); await service.assignRoleToApiKey({ apiKeyId: mockApiKeyId, @@ -238,20 +171,12 @@ describe('ApiKeyRoleService', () => { workspaceId: mockWorkspaceId, }); - expect(mockDataSource.transaction).toHaveBeenCalled(); - expect(mockManagerDelete).toHaveBeenCalledWith(RoleTargetsEntity, { - apiKeyId: mockApiKeyId, - workspaceId: mockWorkspaceId, - }); - expect(mockManagerCreate).toHaveBeenCalledWith(RoleTargetsEntity, { - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }); - expect(mockManagerSave).toHaveBeenCalledWith(mockNewRoleTarget); - expect( - mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache, - ).toHaveBeenCalledWith({ + expect(mockRoleTargetService.create).toHaveBeenCalledWith({ + createRoleTargetInput: { + roleId: mockNewRoleId, + targetId: mockApiKeyId, + targetMetadataForeignKey: 'apiKeyId', + }, workspaceId: mockWorkspaceId, }); }); @@ -267,10 +192,7 @@ describe('ApiKeyRoleService', () => { workspaceId: mockWorkspaceId, }); - expect(mockDataSource.transaction).not.toHaveBeenCalled(); - expect( - mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache, - ).not.toHaveBeenCalled(); + expect(mockRoleTargetService.create).not.toHaveBeenCalled(); }); it('should throw exception if API key not found', async () => { @@ -365,18 +287,6 @@ describe('ApiKeyRoleService', () => { }); }); - describe('recomputeCache', () => { - it('should trigger cache recomputation', async () => { - await service.recomputeCache(mockWorkspaceId); - - expect( - mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache, - ).toHaveBeenCalledWith({ - workspaceId: mockWorkspaceId, - }); - }); - }); - describe('getRolesByApiKeys', () => { it('should return empty map for empty API key IDs', async () => { const result = await service.getRolesByApiKeys({ @@ -498,13 +408,13 @@ describe('ApiKeyRoleService', () => { }); describe('error handling', () => { - it('should handle transaction failures gracefully', async () => { + it('should handle roleTargetService failures gracefully', async () => { mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); mockRoleRepository.findOne.mockResolvedValue(mockNewRole); mockRoleTargetsRepository.findOne.mockResolvedValue(null); - mockDataSource.transaction.mockRejectedValue( - new Error('Transaction failed'), + mockRoleTargetService.create.mockRejectedValue( + new Error('Role target creation failed'), ); await expect( @@ -513,41 +423,36 @@ describe('ApiKeyRoleService', () => { roleId: mockNewRoleId, workspaceId: mockWorkspaceId, }), - ).rejects.toThrow('Transaction failed'); - - expect( - mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache, - ).not.toHaveBeenCalled(); + ).rejects.toThrow('Role target creation failed'); }); - it('should handle cache service failures gracefully', async () => { + it('should throw exception if role cannot be assigned to API keys', async () => { + const roleNotForApiKeys = { + ...mockNewRole, + canBeAssignedToApiKeys: false, + }; + mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(mockNewRole); + mockRoleRepository.findOne.mockResolvedValue(roleNotForApiKeys); mockRoleTargetsRepository.findOne.mockResolvedValue(null); - mockDataSource.transaction.mockImplementation( - async (callback: (manager: any) => Promise) => { - const mockManager = { - delete: jest.fn().mockResolvedValue({ affected: 1 }), - create: jest.fn().mockReturnValue(mockNewRoleTarget), - save: jest.fn().mockResolvedValue(mockNewRoleTarget), - }; - - return await callback(mockManager); - }, - ); - - mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache.mockRejectedValue( - new Error('Cache update failed'), - ); - await expect( service.assignRoleToApiKey({ apiKeyId: mockApiKeyId, roleId: mockNewRoleId, workspaceId: mockWorkspaceId, }), - ).rejects.toThrow('Cache update failed'); + ).rejects.toThrow(ApiKeyException); + + await expect( + service.assignRoleToApiKey({ + apiKeyId: mockApiKeyId, + roleId: mockNewRoleId, + workspaceId: mockWorkspaceId, + }), + ).rejects.toMatchObject({ + code: ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS, + }); }); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts index e6c10a1822..41c0a0f629 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts @@ -1,20 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; -import { - DataSource, - type EntityManager, - In, - IsNull, - Not, - Repository, -} from 'typeorm'; +import { In, IsNull, Not, Repository } from 'typeorm'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { ApiKeyException, ApiKeyExceptionCode, } from 'src/engine/core-modules/api-key/api-key.exception'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -32,8 +26,7 @@ export class ApiKeyRoleService { @InjectRepository(ApiKeyEntity) private readonly apiKeyRepository: Repository, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, - @InjectDataSource() - private readonly dataSource: DataSource, + private readonly roleTargetService: RoleTargetService, ) {} public async assignRoleToApiKey({ @@ -55,45 +48,16 @@ export class ApiKeyRoleService { return; } - await this.dataSource.transaction(async (manager) => { - await this.assignRoleToApiKeyWithManager(manager, { - apiKeyId, + await this.roleTargetService.create({ + createRoleTargetInput: { roleId, - workspaceId, - }); - }); - - await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({ + targetId: apiKeyId, + targetMetadataForeignKey: 'apiKeyId', + }, workspaceId, }); } - public async assignRoleToApiKeyWithManager( - manager: EntityManager, - { - apiKeyId, - roleId, - workspaceId, - }: { - apiKeyId: string; - roleId: string; - workspaceId: string; - }, - ): Promise { - await manager.delete(RoleTargetsEntity, { - apiKeyId, - workspaceId, - }); - - const roleTarget = manager.create(RoleTargetsEntity, { - apiKeyId, - roleId, - workspaceId, - }); - - await manager.save(roleTarget); - } - async getRoleIdForApiKey( apiKeyId: string, workspaceId: string, @@ -115,12 +79,6 @@ export class ApiKeyRoleService { return roleId; } - async recomputeCache(workspaceId: string): Promise { - await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({ - workspaceId, - }); - } - private async validateAssignRoleInput({ apiKeyId, workspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts index 4d1cc054c2..22c9a36ebb 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts @@ -10,6 +10,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @@ -26,11 +27,12 @@ import { ApiKeyController } from './controllers/api-key.controller'; WorkspaceEntity, ]), JwtModule, - TokenModule, WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, - PermissionsModule, FeatureFlagModule, + RoleTargetModule, + TokenModule, + PermissionsModule, ], providers: [ApiKeyService, ApiKeyResolver, ApiKeyRoleService], controllers: [ApiKeyController], diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.spec.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.spec.ts index 6da0d4d7c5..e5f72c528c 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.spec.ts @@ -10,6 +10,7 @@ import { } from 'src/engine/core-modules/api-key/api-key.exception'; import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { ApiKeyEntity } from './api-key.entity'; @@ -21,6 +22,7 @@ describe('ApiKeyService', () => { let mockRoleTargetsRepository: any; let mockJwtWrapperService: any; let mockApiKeyRoleService: any; + let mockRoleTargetService: any; let mockDataSource: any; const mockWorkspaceId = 'workspace-123'; @@ -75,6 +77,10 @@ describe('ApiKeyService', () => { assignRoleToApiKeyWithManager: jest.fn(), }; + mockRoleTargetService = { + create: jest.fn(), + }; + mockDataSource = { transaction: jest.fn(), }; @@ -90,6 +96,10 @@ describe('ApiKeyService', () => { provide: JwtWrapperService, useValue: mockJwtWrapperService, }, + { + provide: RoleTargetService, + useValue: mockRoleTargetService, + }, { provide: getRepositoryToken(RoleTargetsEntity), useValue: mockRoleTargetsRepository, @@ -117,7 +127,7 @@ describe('ApiKeyService', () => { }); describe('create', () => { - it('should create and save an API key using transaction', async () => { + it('should create and save an API key and assign role', async () => { const apiKeyData = { name: 'New API Key', expiresAt: new Date('2025-12-31'), @@ -131,50 +141,26 @@ describe('ApiKeyService', () => { workspaceId: mockWorkspaceId, }; - mockApiKeyRoleService.assignRoleToApiKeyWithManager.mockResolvedValue( - undefined, - ); - mockApiKeyRoleService.recomputeCache.mockResolvedValue(undefined); - - const mockManagerCreate = jest.fn().mockReturnValue(mockApiKey); - const mockManagerSave = jest.fn().mockResolvedValue(mockApiKey); - - mockDataSource.transaction.mockImplementation( - async (callback: (manager: any) => Promise) => { - const mockManager = { - create: mockManagerCreate, - save: mockManagerSave, - }; - - return await callback(mockManager); - }, - ); + mockApiKeyRepository.save.mockResolvedValue(mockApiKey); + mockRoleTargetService.create.mockResolvedValue(undefined); const result = await service.create(apiKeyData); - expect(mockDataSource.transaction).toHaveBeenCalled(); - expect(mockManagerCreate).toHaveBeenCalledWith( - ApiKeyEntity, + expect(mockApiKeyRepository.save).toHaveBeenCalledWith( expectedApiKeyFields, ); - expect(mockManagerSave).toHaveBeenCalledWith(mockApiKey); - expect( - mockApiKeyRoleService.assignRoleToApiKeyWithManager, - ).toHaveBeenCalledWith( - expect.any(Object), // manager - { - apiKeyId: mockApiKey.id, + expect(mockRoleTargetService.create).toHaveBeenCalledWith({ + createRoleTargetInput: { roleId: 'mock-role-id', - workspaceId: mockWorkspaceId, + targetId: mockApiKey.id, + targetMetadataForeignKey: 'apiKeyId', }, - ); - expect(mockApiKeyRoleService.recomputeCache).toHaveBeenCalledWith( - mockWorkspaceId, - ); + workspaceId: mockWorkspaceId, + }); expect(result).toEqual(mockApiKey); }); - it('should handle role assignment failures within transaction', async () => { + it('should delete API key if role assignment fails', async () => { const apiKeyData = { name: 'New API Key', expiresAt: new Date('2025-12-31'), @@ -182,38 +168,22 @@ describe('ApiKeyService', () => { roleId: 'mock-role-id', }; - const mockManagerCreate = jest.fn().mockReturnValue(mockApiKey); - const mockManagerSave = jest.fn().mockResolvedValue(mockApiKey); - - mockApiKeyRoleService.assignRoleToApiKeyWithManager.mockRejectedValue( + mockApiKeyRepository.save.mockResolvedValue(mockApiKey); + mockApiKeyRepository.delete = jest.fn().mockResolvedValue(undefined); + mockRoleTargetService.create.mockRejectedValue( new Error('Role assignment failed'), ); - mockDataSource.transaction.mockImplementation( - async (callback: (manager: any) => Promise) => { - const mockManager = { - create: mockManagerCreate, - save: mockManagerSave, - }; - - return await callback(mockManager); - }, - ); - await expect(service.create(apiKeyData)).rejects.toThrow( 'Role assignment failed', ); - expect(mockDataSource.transaction).toHaveBeenCalled(); - expect(mockManagerCreate).toHaveBeenCalled(); - expect(mockManagerSave).toHaveBeenCalled(); - expect( - mockApiKeyRoleService.assignRoleToApiKeyWithManager, - ).toHaveBeenCalled(); - expect(mockApiKeyRoleService.recomputeCache).not.toHaveBeenCalled(); + expect(mockApiKeyRepository.save).toHaveBeenCalled(); + expect(mockRoleTargetService.create).toHaveBeenCalled(); + expect(mockApiKeyRepository.delete).toHaveBeenCalledWith(mockApiKey.id); }); - it('should handle transaction failures gracefully', async () => { + it('should handle save failures gracefully', async () => { const apiKeyData = { name: 'New API Key', expiresAt: new Date('2025-12-31'), @@ -221,19 +191,12 @@ describe('ApiKeyService', () => { roleId: 'mock-role-id', }; - mockDataSource.transaction.mockRejectedValue( - new Error('Transaction failed'), - ); + mockApiKeyRepository.save.mockRejectedValue(new Error('Save failed')); - await expect(service.create(apiKeyData)).rejects.toThrow( - 'Transaction failed', - ); + await expect(service.create(apiKeyData)).rejects.toThrow('Save failed'); - expect(mockDataSource.transaction).toHaveBeenCalled(); - expect( - mockApiKeyRoleService.assignRoleToApiKeyWithManager, - ).not.toHaveBeenCalled(); - expect(mockApiKeyRoleService.recomputeCache).not.toHaveBeenCalled(); + expect(mockApiKeyRepository.save).toHaveBeenCalled(); + expect(mockRoleTargetService.create).not.toHaveBeenCalled(); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.ts index 683cc7ec49..d04f0aee2c 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.service.ts @@ -1,11 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; -import { DataSource, IsNull, Repository } from 'typeorm'; +import { IsNull, Repository } from 'typeorm'; import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { ApiKeyException, @@ -14,6 +13,7 @@ import { import { type ApiKeyToken } from 'src/engine/core-modules/auth/dto/api-key-token.dto'; import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; @Injectable() export class ApiKeyService { @@ -21,34 +21,30 @@ export class ApiKeyService { @InjectRepository(ApiKeyEntity) private readonly apiKeyRepository: Repository, private readonly jwtWrapperService: JwtWrapperService, - private readonly apiKeyRoleService: ApiKeyRoleService, - @InjectDataSource() - private readonly dataSource: DataSource, + private readonly roleTargetService: RoleTargetService, ) {} async create( apiKeyData: Partial & { roleId: string }, ): Promise { const { roleId, ...apiKeyFields } = apiKeyData; + const savedApiKey = await this.apiKeyRepository.save(apiKeyFields); - return await this.dataSource - .transaction(async (manager) => { - const apiKey = manager.create(ApiKeyEntity, apiKeyFields); - const savedApiKey = await manager.save(apiKey); - - await this.apiKeyRoleService.assignRoleToApiKeyWithManager(manager, { - apiKeyId: savedApiKey.id, + try { + await this.roleTargetService.create({ + createRoleTargetInput: { roleId, - workspaceId: savedApiKey.workspaceId, - }); - - return savedApiKey; - }) - .then(async (savedApiKey) => { - await this.apiKeyRoleService.recomputeCache(savedApiKey.workspaceId); - - return savedApiKey; + targetId: savedApiKey.id, + targetMetadataForeignKey: 'apiKeyId', + }, + workspaceId: savedApiKey.workspaceId, }); + } catch (error) { + await this.apiKeyRepository.delete(savedApiKey.id); + throw error; + } + + return savedApiKey; } async findById( diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts index 116a6180e4..04237f030a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; -import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver'; import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver'; import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module'; import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; @@ -14,8 +14,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module'; import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module'; import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; +import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; -import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; @Module({ imports: [ @@ -36,7 +36,7 @@ import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadat providers: [ ApplicationResolver, ApplicationSyncService, - ObjectMetadataGraphqlApiExceptionInterceptor, + WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, ], exports: [ApplicationSyncService], }) diff --git a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts index 8f2ebe7def..f4e22e8a03 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts @@ -15,14 +15,14 @@ import { RequireFeatureFlag } 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 { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; -import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; +import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; @UseGuards( WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.APPLICATIONS), ) @Resolver() -@UseInterceptors(ObjectMetadataGraphqlApiExceptionInterceptor) +@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) @UseFilters(ApplicationExceptionFilter) export class ApplicationResolver { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index b42450254b..bf65107747 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -430,40 +430,31 @@ describe('UserWorkspaceService', () => { workspace.id, ); expect(service.create).toHaveBeenCalled(); - expect(service.create).toHaveBeenCalledWith( - { - workspaceId: workspace.id, - userId: user.id, - isExistingUser: true, - }, - undefined, - ); + expect(service.create).toHaveBeenCalledWith({ + workspaceId: workspace.id, + userId: user.id, + isExistingUser: true, + }); expect(service.createWorkspaceMember).toHaveBeenCalledWith( workspace.id, user, ); - expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith( - { - workspaceId: workspace.id, - userWorkspaceId: userWorkspace.id, - roleId: workspace.defaultRoleId, - }, - undefined, - ); + expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({ + workspaceId: workspace.id, + userWorkspaceId: userWorkspace.id, + roleId: workspace.defaultRoleId, + }); expect( workspaceInvitationService.invalidateWorkspaceInvitation, - ).toHaveBeenCalledWith(workspace.id, user.email, undefined); + ).toHaveBeenCalledWith(workspace.id, user.email); expect( onboardingService.setOnboardingCreateProfilePending, - ).toHaveBeenCalledWith( - { - userId: user.id, - workspaceId: workspace.id, - value: true, - }, - undefined, - ); + ).toHaveBeenCalledWith({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); }); it('should not add user to workspace if already in workspace', async () => { diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index f23b8320cf..8be3136dd4 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -133,7 +133,6 @@ export class UserWorkspaceService extends TypeOrmQueryService String, { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/agent-role.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/agent-role.service.spec.ts index 2687293d68..f62a2cd0a2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/agent-role.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/agent-role.service.spec.ts @@ -9,6 +9,8 @@ import { } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -19,6 +21,7 @@ describe('AiAgentRoleService', () => { let agentRepository: Repository; let roleRepository: Repository; let roleTargetsRepository: Repository; + let roleTargetService: RoleTargetService; const testWorkspaceId = 'test-workspace-id'; let testAgent: AgentEntity; @@ -52,6 +55,13 @@ describe('AiAgentRoleService', () => { find: jest.fn(), }, }, + { + provide: RoleTargetService, + useValue: { + create: jest.fn(), + delete: jest.fn(), + }, + }, ], }).compile(); @@ -65,6 +75,7 @@ describe('AiAgentRoleService', () => { roleTargetsRepository = module.get>( getRepositoryToken(RoleTargetsEntity), ); + roleTargetService = module.get(RoleTargetService); // Setup test data testAgent = { @@ -122,24 +133,12 @@ describe('AiAgentRoleService', () => { describe('assignRoleToAgent', () => { it('should successfully assign a role to an agent', async () => { // Arrange - const newRoleTarget = { - id: 'new-role-target-id', - roleId: testRole.id, - agentId: testAgent.id, - workspaceId: testWorkspaceId, - createdAt: new Date(), - updatedAt: new Date(), - } as RoleTargetsEntity; - jest.spyOn(agentRepository, 'findOne').mockResolvedValue(testAgent); jest.spyOn(roleRepository, 'findOne').mockResolvedValue(testRole); jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null); jest - .spyOn(roleTargetsRepository, 'save') - .mockResolvedValue(newRoleTarget); - jest - .spyOn(roleTargetsRepository, 'delete') - .mockResolvedValue({ affected: 0 } as any); + .spyOn(roleTargetService, 'create') + .mockResolvedValue({} as FlatRoleTarget); // Act await service.assignRoleToAgent({ @@ -162,38 +161,24 @@ describe('AiAgentRoleService', () => { workspaceId: testWorkspaceId, }, }); - expect(roleTargetsRepository.save).toHaveBeenCalledWith({ - roleId: testRole.id, - agentId: testAgent.id, + expect(roleTargetService.create).toHaveBeenCalledWith({ + createRoleTargetInput: { + roleId: testRole.id, + targetId: testAgent.id, + targetMetadataForeignKey: 'agentId', + }, workspaceId: testWorkspaceId, }); - expect(roleTargetsRepository.delete).toHaveBeenCalledWith({ - agentId: testAgent.id, - workspaceId: testWorkspaceId, - id: expect.any(Object), // Not(newRoleTarget.id) - }); }); it('should replace existing role when assigning a new role to an agent', async () => { // Arrange - const newRoleTarget = { - id: 'new-role-target-id', - roleId: testRole2.id, - agentId: testAgent.id, - workspaceId: testWorkspaceId, - createdAt: new Date(), - updatedAt: new Date(), - } as RoleTargetsEntity; - jest.spyOn(agentRepository, 'findOne').mockResolvedValue(testAgent); jest.spyOn(roleRepository, 'findOne').mockResolvedValue(testRole2); jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null); jest - .spyOn(roleTargetsRepository, 'save') - .mockResolvedValue(newRoleTarget); - jest - .spyOn(roleTargetsRepository, 'delete') - .mockResolvedValue({ affected: 1 } as any); + .spyOn(roleTargetService, 'create') + .mockResolvedValue({} as FlatRoleTarget); // Act await service.assignRoleToAgent({ @@ -203,16 +188,14 @@ describe('AiAgentRoleService', () => { }); // Assert - expect(roleTargetsRepository.save).toHaveBeenCalledWith({ - roleId: testRole2.id, - agentId: testAgent.id, + expect(roleTargetService.create).toHaveBeenCalledWith({ + createRoleTargetInput: { + roleId: testRole2.id, + targetId: testAgent.id, + targetMetadataForeignKey: 'agentId', + }, workspaceId: testWorkspaceId, }); - expect(roleTargetsRepository.delete).toHaveBeenCalledWith({ - agentId: testAgent.id, - workspaceId: testWorkspaceId, - id: expect.any(Object), // Not(newRoleTarget.id) - }); }); it('should not create duplicate role target when assigning the same role', async () => { @@ -240,8 +223,7 @@ describe('AiAgentRoleService', () => { }); // Assert - expect(roleTargetsRepository.save).not.toHaveBeenCalled(); - expect(roleTargetsRepository.delete).not.toHaveBeenCalled(); + expect(roleTargetService.create).not.toHaveBeenCalled(); }); it('should throw AgentException when agent does not exist', async () => { @@ -330,9 +312,19 @@ describe('AiAgentRoleService', () => { describe('removeRoleFromAgent', () => { it('should successfully remove role from agent', async () => { // Arrange + const existingRoleTarget = { + id: 'existing-role-target-id', + roleId: testRole.id, + agentId: testAgent.id, + workspaceId: testWorkspaceId, + createdAt: new Date(), + updatedAt: new Date(), + } as RoleTargetsEntity; + jest - .spyOn(roleTargetsRepository, 'delete') - .mockResolvedValue({ affected: 1 } as any); + .spyOn(roleTargetsRepository, 'findOne') + .mockResolvedValue(existingRoleTarget); + jest.spyOn(roleTargetService, 'delete').mockResolvedValue(undefined); // Act await service.removeRoleFromAgent({ @@ -341,29 +333,38 @@ describe('AiAgentRoleService', () => { }); // Assert - expect(roleTargetsRepository.delete).toHaveBeenCalledWith({ - agentId: testAgent.id, + expect(roleTargetsRepository.findOne).toHaveBeenCalledWith({ + where: { + agentId: testAgent.id, + workspaceId: testWorkspaceId, + }, + }); + expect(roleTargetService.delete).toHaveBeenCalledWith({ + id: existingRoleTarget.id, workspaceId: testWorkspaceId, }); }); - it('should not throw error when removing role from agent that has no role', async () => { + it('should throw error when removing role from agent that has no role', async () => { // Arrange - jest - .spyOn(roleTargetsRepository, 'delete') - .mockResolvedValue({ affected: 0 } as any); + jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null); - // Act & Assert - Should not throw + // Act & Assert await expect( service.removeRoleFromAgent({ workspaceId: testWorkspaceId, agentId: testAgent.id, }), - ).resolves.not.toThrow(); + ).rejects.toThrow(AgentException); - expect(roleTargetsRepository.delete).toHaveBeenCalledWith({ - agentId: testAgent.id, - workspaceId: testWorkspaceId, + await expect( + service.removeRoleFromAgent({ + workspaceId: testWorkspaceId, + agentId: testAgent.id, + }), + ).rejects.toMatchObject({ + code: AgentExceptionCode.ROLE_NOT_FOUND, + message: `Role target not found for agent ${testAgent.id}`, }); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module.ts index b19418bb74..6e983be299 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; +import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -10,6 +11,7 @@ import { AiAgentRoleService } from './ai-agent-role.service'; @Module({ imports: [ TypeOrmModule.forFeature([AgentEntity, RoleEntity, RoleTargetsEntity]), + RoleTargetModule, ], providers: [AiAgentRoleService], exports: [AiAgentRoleService], diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service.ts index efceab02db..afa6ce2074 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service.ts @@ -9,6 +9,7 @@ import { AgentExceptionCode, } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -21,6 +22,7 @@ export class AiAgentRoleService { private readonly roleRepository: Repository, @InjectRepository(RoleTargetsEntity) private readonly roleTargetsRepository: Repository, + private readonly roleTargetService: RoleTargetService, ) {} public async assignRoleToAgent({ @@ -42,17 +44,14 @@ export class AiAgentRoleService { return; } - const newRoleTarget = await this.roleTargetsRepository.save({ - roleId, - agentId, + await this.roleTargetService.create({ + createRoleTargetInput: { + roleId, + targetId: agentId, + targetMetadataForeignKey: 'agentId', + }, workspaceId, }); - - await this.roleTargetsRepository.delete({ - agentId, - workspaceId, - id: Not(newRoleTarget.id), - }); } public async assignStandardRoleToAgent({ @@ -68,7 +67,7 @@ export class AiAgentRoleService { where: { standardId: standardRoleId, workspaceId }, }); - if (!role) { + if (!isDefined(role)) { throw new AgentException( `Standard role with standard ID ${standardRoleId} not found in workspace`, AgentExceptionCode.ROLE_NOT_FOUND, @@ -89,8 +88,22 @@ export class AiAgentRoleService { workspaceId: string; agentId: string; }): Promise { - await this.roleTargetsRepository.delete({ - agentId, + const existingRoleTarget = await this.roleTargetsRepository.findOne({ + where: { + agentId, + workspaceId, + }, + }); + + if (!isDefined(existingRoleTarget)) { + throw new AgentException( + `Role target not found for agent ${agentId}`, + AgentExceptionCode.ROLE_NOT_FOUND, + ); + } + + await this.roleTargetService.delete({ + id: existingRoleTarget.id, workspaceId, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts index 5756b9add7..f7140b9a50 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts @@ -1,4 +1,4 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; @@ -9,7 +9,6 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file- import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module'; import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module'; import { AiChatRouterModule } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.module'; @@ -54,7 +53,6 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi TokenModule, WorkspaceDomainsModule, WorkflowToolsModule, - forwardRef(() => UserModule), UserWorkspaceModule, UserRoleModule, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index b294056411..6b3ee7f6c2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -5,6 +5,8 @@ import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/meta import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant'; +import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant'; +import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant'; import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant'; import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant'; import { FLAT_VIEW_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/flat-view-group-editable-properties.constant'; @@ -87,6 +89,14 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { ], propertiesToStringify: ['value'], }, + role: { + propertiesToCompare: [...FLAT_ROLE_EDITABLE_PROPERTIES], + propertiesToStringify: [], + }, + roleTarget: { + propertiesToCompare: [...FLAT_ROLE_TARGET_EDITABLE_PROPERTIES], + propertiesToStringify: [], + }, } as const satisfies { [P in AllMetadataName]: OneFlatEntityConfiguration

; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts index f34ff88889..d1d03e3b75 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts @@ -101,4 +101,11 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = { flatEntityForeignKeyAggregator: 'viewFilterIds', }, }, + role: {}, + roleTarget: { + roleId: { + metadataName: 'role', + flatEntityForeignKeyAggregator: 'roleTargetIds', + }, + }, } as const satisfies MetadataNameAndRelations; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant.ts index ba65f2c301..ac002623e3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant.ts @@ -10,4 +10,6 @@ export const ALL_METADATA_NAME = { databaseEventTrigger: 'databaseEventTrigger', routeTrigger: 'routeTrigger', viewFilter: 'viewFilter', + role: 'role', + roleTarget: 'roleTarget', } as const; 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 651efd9f10..bd20f06ed8 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 @@ -49,4 +49,8 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { fieldMetadata: true, view: true, }, + role: {}, + roleTarget: { + role: true, + }, } as const satisfies MetadataRequiredForValidation; 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 ca7ae77680..3cf41e114d 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 @@ -6,12 +6,16 @@ import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-meta import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.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'; import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type'; import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type'; import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { type RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; @@ -44,6 +48,16 @@ import { type DeleteObjectAction, type UpdateObjectAction, } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/types/workspace-migration-object-action-v2'; +import { + type CreateRoleTargetAction, + type DeleteRoleTargetAction, + type UpdateRoleTargetAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type'; +import { + type CreateRoleAction, + type DeleteRoleAction, + type UpdateRoleAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type'; import { type CreateRouteTriggerAction, type DeleteRouteTriggerAction, @@ -175,4 +189,22 @@ export type AllFlatEntityTypesByMetadataName = { flatEntity: FlatViewFilter; entity: ViewFilterEntity; }; + role: { + actions: { + created: CreateRoleAction; + updated: UpdateRoleAction; + deleted: DeleteRoleAction; + }; + flatEntity: FlatRole; + entity: RoleEntity; + }; + roleTarget: { + actions: { + created: CreateRoleTargetAction; + updated: UpdateRoleTargetAction; + deleted: DeleteRoleTargetAction; + }; + flatEntity: FlatRoleTarget; + entity: RoleTargetsEntity; + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant.ts new file mode 100644 index 0000000000..d9a9ef0ab6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant.ts @@ -0,0 +1,7 @@ +import { ROLE_TARGET_FOREIGN_KEY_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; + +export const FLAT_ROLE_TARGET_EDITABLE_PROPERTIES = [ + 'roleId', + ...ROLE_TARGET_FOREIGN_KEY_PROPERTIES, +] as const satisfies (keyof FlatRoleTarget)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant.ts new file mode 100644 index 0000000000..744a18d890 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant.ts @@ -0,0 +1,7 @@ +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; + +export const ROLE_TARGET_FOREIGN_KEY_PROPERTIES = [ + 'userWorkspaceId', + 'apiKeyId', + 'agentId', +] as const satisfies (keyof FlatRoleTarget)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts new file mode 100644 index 0000000000..0c62a21fae --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type'; +import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util'; +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; +import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; + +@Injectable() +@WorkspaceFlatMapCache('flatRoleTargetMaps') +export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceFlatMapCacheService { + constructor( + @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) + cacheStorageService: CacheStorageService, + @InjectRepository(RoleTargetsEntity) + private readonly roleTargetsRepository: Repository, + ) { + super(cacheStorageService); + } + + protected async computeFlatMap({ + workspaceId, + }: { + workspaceId: string; + }): Promise { + const roleTargets = await this.roleTargetsRepository.find({ + where: { + workspaceId, + }, + withDeleted: true, + }); + + const flatRoleTargetMaps = createEmptyFlatEntityMaps(); + + for (const roleTargetEntity of roleTargets) { + const flatRoleTarget = + fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatRoleTarget, + flatEntityMapsToMutate: flatRoleTargetMaps, + }); + } + + return flatRoleTargetMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type.ts new file mode 100644 index 0000000000..913d67b2d7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; + +export type FlatRoleTargetMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target.type.ts new file mode 100644 index 0000000000..a153693930 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/flat-role-target.type.ts @@ -0,0 +1,12 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; +import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; + +export const roleTargetsEntityRelationProperties = ['role', 'apiKey'] as const; + +export type RoleTargetsEntityRelationProperties = + (typeof roleTargetsEntityRelationProperties)[number]; + +export type FlatRoleTarget = FlatEntityFrom< + RoleTargetsEntity, + RoleTargetsEntityRelationProperties +>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type.ts new file mode 100644 index 0000000000..6a08e8ec37 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type.ts @@ -0,0 +1,4 @@ +import { type ROLE_TARGET_FOREIGN_KEY_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant'; + +export type RoleTargetForeignKeyProperties = + (typeof ROLE_TARGET_FOREIGN_KEY_PROPERTIES)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util.ts new file mode 100644 index 0000000000..9ffad08bdd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util.ts @@ -0,0 +1,23 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { type RoleTargetForeignKeyProperties } from 'src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type'; + +type FindAllFlatRoleTargetOfArgs = { + targetMetadataForeignKey: RoleTargetForeignKeyProperties; + targetId: string; +} & Pick; +export const findFlatRoleTargetFromForeignKey = ({ + flatRoleTargetMaps, + targetMetadataForeignKey, + targetId, +}: FindAllFlatRoleTargetOfArgs): FlatRoleTarget | undefined => { + const allRoleTargets = Object.values(flatRoleTargetMaps.byId).filter( + isDefined, + ); + + return allRoleTargets.find( + (roleTarget) => roleTarget[targetMetadataForeignKey] === targetId, + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts new file mode 100644 index 0000000000..9d90621ff8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts @@ -0,0 +1,19 @@ +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; + +export const fromRoleTargetsEntityToFlatRoleTarget = ( + roleTarget: RoleTargetsEntity, +): FlatRoleTarget => { + return { + id: roleTarget.id, + workspaceId: roleTarget.workspaceId, + roleId: roleTarget.roleId, + userWorkspaceId: roleTarget.userWorkspaceId, + agentId: roleTarget.agentId, + apiKeyId: roleTarget.apiKeyId, + applicationId: roleTarget.applicationId, + universalIdentifier: roleTarget.universalIdentifier ?? roleTarget.id, + createdAt: roleTarget.createdAt, + updatedAt: roleTarget.updatedAt, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts new file mode 100644 index 0000000000..4338974bce --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts @@ -0,0 +1,16 @@ +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; + +export const FLAT_ROLE_EDITABLE_PROPERTIES: (keyof FlatRole)[] = [ + 'label', + 'description', + 'icon', + 'canUpdateAllSettings', + 'canAccessAllTools', + 'canReadAllObjectRecords', + 'canUpdateAllObjectRecords', + 'canSoftDeleteAllObjectRecords', + 'canDestroyAllObjectRecords', + 'canBeAssignedToUsers', + 'canBeAssignedToAgents', + 'canBeAssignedToApiKeys', +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants.ts new file mode 100644 index 0000000000..cdb4013821 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants.ts @@ -0,0 +1,11 @@ +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; + +export const FLAT_ROLE_REQUIRED_PROPERTIES = [ + 'label', + 'canUpdateAllSettings', + 'canAccessAllTools', + 'canReadAllObjectRecords', + 'canUpdateAllObjectRecords', + 'canSoftDeleteAllObjectRecords', + 'canDestroyAllObjectRecords', +] as const satisfies (keyof FlatRole)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts index 4eb1cff8e6..022f6dcec6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts @@ -1,17 +1,28 @@ +import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; +import { type FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; +import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { type PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; - -export const roleEntityRelationProperties = [ - 'roleTargets', - 'objectPermissions', - 'permissionFlags', - 'fieldPermissions', -] as const; +import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type'; export type RoleEntityRelationProperties = - (typeof roleEntityRelationProperties)[number]; + ExtractRecordTypeOrmRelationProperties< + RoleEntity, + | RoleTargetsEntity + | ObjectPermissionEntity + | PermissionFlagEntity + | FieldPermissionEntity + | ApplicationEntity + >; export type FlatRole = FlatEntityFrom< RoleEntity, - RoleEntityRelationProperties | 'createdAt' | 'updatedAt' ->; + RoleEntityRelationProperties +> & { + roleTargetIds: string[]; + objectPermissionIds: string[]; + permissionFlagIds: string[]; + fieldPermissionIds: string[]; +}; 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 new file mode 100644 index 0000000000..33415bee33 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts @@ -0,0 +1,58 @@ +import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { type CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; + +export const fromCreateRoleInputToFlatRoleToCreate = ({ + createRoleInput, + workspaceId, + applicationId, +}: { + createRoleInput: CreateRoleInput; + workspaceId: string; + applicationId: string; +}): FlatRole => { + const now = new Date(); + + const { + label, + description, + icon, + id: inputId, + } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + createRoleInput, + ['description', 'icon', 'id', 'label'], + ); + const id = inputId ?? v4(); + + return { + id, + standardId: null, + label, + description: description ?? null, + icon: icon ?? null, + canUpdateAllSettings: createRoleInput.canUpdateAllSettings ?? false, + canAccessAllTools: createRoleInput.canAccessAllTools ?? false, + canReadAllObjectRecords: createRoleInput.canReadAllObjectRecords ?? false, + canUpdateAllObjectRecords: + createRoleInput.canUpdateAllObjectRecords ?? false, + canSoftDeleteAllObjectRecords: + createRoleInput.canSoftDeleteAllObjectRecords ?? false, + canDestroyAllObjectRecords: + createRoleInput.canDestroyAllObjectRecords ?? false, + canBeAssignedToUsers: createRoleInput.canBeAssignedToUsers ?? true, + canBeAssignedToAgents: createRoleInput.canBeAssignedToAgents ?? true, + canBeAssignedToApiKeys: createRoleInput.canBeAssignedToApiKeys ?? true, + isEditable: true, + workspaceId, + createdAt: now, + updatedAt: now, + universalIdentifier: id, + applicationId, + roleTargetIds: [], + objectPermissionIds: [], + permissionFlagIds: [], + fieldPermissionIds: [], + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util.ts new file mode 100644 index 0000000000..fb3a457a8f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util.ts @@ -0,0 +1,35 @@ +import { msg } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { + PermissionsException, + PermissionsExceptionCode, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; + +export const fromDeleteRoleInputToFlatRoleOrThrow = ({ + flatRoleMaps, + roleId, +}: { + flatRoleMaps: FlatEntityMaps; + roleId: string; +}): FlatRole => { + const existingFlatRole = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: roleId, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(existingFlatRole)) { + throw new PermissionsException( + 'Role not found', + PermissionsExceptionCode.ROLE_NOT_FOUND, + { + userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`, + }, + ); + } + + return existingFlatRole; +}; 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 d824ef0ed9..edb516075d 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 @@ -19,7 +19,13 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => { canBeAssignedToAgents: role.canBeAssignedToAgents, canBeAssignedToApiKeys: role.canBeAssignedToApiKeys, workspaceId: role.workspaceId, - universalIdentifier: role.standardId || role.id, + createdAt: role.createdAt, + updatedAt: role.updatedAt, + universalIdentifier: role.universalIdentifier ?? role.standardId ?? role.id, applicationId: role.applicationId ?? null, + roleTargetIds: role.roleTargets.map((rt) => rt.id), + objectPermissionIds: role.objectPermissions.map((op) => op.id), + permissionFlagIds: role.permissionFlags.map((pf) => pf.id), + fieldPermissionIds: role.fieldPermissions.map((fp) => fp.id), }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts index 9a4236b4a7..b754e70e4c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts @@ -7,10 +7,18 @@ export const fromStandardRoleDefinitionToFlatRole = ( standardRoleDefinition: StandardRoleDefinition, workspaceId: string, ): FlatRole => { + const createdAt = new Date(); + return { ...standardRoleDefinition, id: v4(), workspaceId, universalIdentifier: standardRoleDefinition.standardId || v4(), + createdAt, + updatedAt: createdAt, + permissionFlagIds: [], + fieldPermissionIds: [], + objectPermissionIds: [], + roleTargetIds: [], }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util.ts new file mode 100644 index 0000000000..1bfa554a0f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util.ts @@ -0,0 +1,39 @@ +import { msg } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { + PermissionsException, + PermissionsExceptionCode, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { type UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; + +export const fromUpdateRoleInputToFlatRoleToUpdateOrThrow = ({ + flatRoleMaps, + updateRoleInput, +}: { + flatRoleMaps: FlatEntityMaps; + updateRoleInput: UpdateRoleInput; +}): FlatRole => { + const existingFlatRole = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: updateRoleInput.id, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(existingFlatRole)) { + throw new PermissionsException( + 'Role not found', + PermissionsExceptionCode.ROLE_NOT_FOUND, + { + userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`, + }, + ); + } + + return { + ...existingFlatRole, + ...updateRoleInput.update, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts index f9a0a84ad1..4161720e59 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts @@ -7,6 +7,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -24,8 +25,9 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor TypeOrmModule.forFeature([UserWorkspaceEntity]), UserRoleModule, WorkspacePermissionsCacheModule, + RoleTargetModule, ], - providers: [PermissionsService, ApiKeyRoleService], + providers: [ApiKeyRoleService, PermissionsService], exports: [PermissionsService], }) export class PermissionsModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.module.ts b/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.module.ts new file mode 100644 index 0000000000..a94d50dc0b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; + +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; + +import { RoleTargetService } from './services/role-target.service'; + +@Module({ + imports: [ + WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceMigrationV2Module, + ApplicationModule, + ], + providers: [RoleTargetService], + exports: [RoleTargetService], +}) +export class RoleTargetModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/role-target/services/role-target.service.ts b/packages/twenty-server/src/engine/metadata-modules/role-target/services/role-target.service.ts new file mode 100644 index 0000000000..9003a39c8c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role-target/services/role-target.service.ts @@ -0,0 +1,173 @@ +import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'twenty-shared/utils'; + +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { CreateRoleTargetInput } from 'src/engine/metadata-modules/role-target/types/create-role-target.input'; +import { fromCreateRoleTargetInputToFlatRoleTargetToCreate } from 'src/engine/metadata-modules/role-target/utils/from-create-role-target-input-to-flat-role-target-to-create.util'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; + +export type DeleteRoleTargetInput = { + id: string; + workspaceId: string; +}; + +export type FindRoleTargetInput = { + id: string; + workspaceId: string; +}; + +@Injectable() +export class RoleTargetService { + constructor( + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly applicationService: ApplicationService, + ) {} + + async create({ + createRoleTargetInput, + workspaceId, + }: { + createRoleTargetInput: CreateRoleTargetInput; + workspaceId: string; + }): Promise { + const { flatRoleTargetMaps, flatRoleMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleTargetMaps', 'flatRoleMaps'], + }, + ); + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatRoleTargetToCreate, flatRoleTargetsToDelete } = + fromCreateRoleTargetInputToFlatRoleTargetToCreate({ + createRoleTargetInput: { + ...createRoleTargetInput, + applicationId: + createRoleTargetInput.applicationId ?? + workspaceCustomFlatApplication.id, + }, + flatRoleTargetMaps, + workspaceId, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromToAllFlatEntityMaps: { + flatRoleTargetMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: flatRoleTargetMaps, + flatEntityToCreate: [flatRoleTargetToCreate], + flatEntityToDelete: flatRoleTargetsToDelete, + flatEntityToUpdate: [], + }), + }, + dependencyAllFlatEntityMaps: { + flatRoleMaps, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: { + roleTarget: true, + }, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while creating role target', + ); + } + + const { flatRoleTargetMaps: recomputedFlatRoleTargetMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleTargetMaps'], + }, + ); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatRoleTargetToCreate.id, + flatEntityMaps: recomputedFlatRoleTargetMaps, + }); + } + + async delete({ id, workspaceId }: DeleteRoleTargetInput): Promise { + const { flatRoleTargetMaps: existingFlatRoleTargetMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleTargetMaps'], + }, + ); + + const roleTargetToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: id, + flatEntityMaps: existingFlatRoleTargetMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromToAllFlatEntityMaps: { + flatRoleTargetMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRoleTargetMaps, + flatEntityToCreate: [], + flatEntityToDelete: [roleTargetToDelete], + flatEntityToUpdate: [], + }), + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: { + roleTarget: true, + }, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting role target', + ); + } + } + + async findOne({ + findRoleTargetInput, + }: { + findRoleTargetInput: FindRoleTargetInput; + }): Promise { + const { workspaceId, id } = findRoleTargetInput; + + const { flatRoleTargetMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleTargetMaps'], + }, + ); + + const roleTarget = flatRoleTargetMaps.byId[id]; + + return roleTarget ?? null; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/role-target/types/create-role-target.input.ts b/packages/twenty-server/src/engine/metadata-modules/role-target/types/create-role-target.input.ts new file mode 100644 index 0000000000..16fb5c0306 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role-target/types/create-role-target.input.ts @@ -0,0 +1,9 @@ +import { type RoleTargetForeignKeyProperties } from 'src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type'; + +export type CreateRoleTargetInput = { + roleId: string; + applicationId?: string; + universalIdentifier?: string; + targetId: string; + targetMetadataForeignKey: RoleTargetForeignKeyProperties; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/role-target/utils/from-create-role-target-input-to-flat-role-target-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/role-target/utils/from-create-role-target-input-to-flat-role-target-to-create.util.ts new file mode 100644 index 0000000000..41cf386404 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role-target/utils/from-create-role-target-input-to-flat-role-target-to-create.util.ts @@ -0,0 +1,50 @@ +import { v4 } from 'uuid'; +import { isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { findFlatRoleTargetFromForeignKey } from 'src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util'; +import { type CreateRoleTargetInput } from 'src/engine/metadata-modules/role-target/types/create-role-target.input'; + +export const fromCreateRoleTargetInputToFlatRoleTargetToCreate = ({ + createRoleTargetInput, + workspaceId, + flatRoleTargetMaps, +}: { + createRoleTargetInput: CreateRoleTargetInput & { applicationId: string }; + workspaceId: string; +} & Pick): { + flatRoleTargetToCreate: FlatRoleTarget; + flatRoleTargetsToDelete: FlatRoleTarget[]; +} => { + const now = new Date(); + const { roleId, targetId, targetMetadataForeignKey, universalIdentifier } = + createRoleTargetInput; + + const flatRoleTargetToCreate: FlatRoleTarget = { + id: v4(), + roleId, + userWorkspaceId: null, + agentId: null, + apiKeyId: null, + createdAt: now, + updatedAt: now, + universalIdentifier: universalIdentifier ?? v4(), + workspaceId, + applicationId: createRoleTargetInput.applicationId, + [targetMetadataForeignKey]: targetId, + }; + + const flatRoleTargetToDelete = findFlatRoleTargetFromForeignKey({ + flatRoleTargetMaps, + targetMetadataForeignKey, + targetId, + }); + + return { + flatRoleTargetToCreate: flatRoleTargetToCreate, + flatRoleTargetsToDelete: isDefined(flatRoleTargetToDelete) + ? [flatRoleTargetToDelete] + : [], + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts index 2c7b2b5872..3a4e796f10 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts @@ -37,10 +37,10 @@ export class RoleDTO { label: string; @Field({ nullable: true }) - description: string; + description?: string; @Field({ nullable: true }) - icon: string; + icon?: string; @Field({ nullable: false }) isEditable: boolean; @@ -55,7 +55,7 @@ export class RoleDTO { canBeAssignedToApiKeys: boolean; @HideField() - roleTargets: Relation; + roleTargets?: Relation; @Field(() => [WorkspaceMemberDTO], { nullable: true }) workspaceMembers?: WorkspaceMemberDTO[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/exceptions/role-target.exception.ts b/packages/twenty-server/src/engine/metadata-modules/role/exceptions/role-target.exception.ts new file mode 100644 index 0000000000..d750512afc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role/exceptions/role-target.exception.ts @@ -0,0 +1,16 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class RoleTargetException extends CustomException { + code: RoleTargetExceptionCode; + constructor(message: string, code: RoleTargetExceptionCode) { + super(message, code); + } +} + +export enum RoleTargetExceptionCode { + ROLE_TARGET_NOT_FOUND = 'ROLE_TARGET_NOT_FOUND', + INVALID_ROLE_TARGET_DATA = 'INVALID_ROLE_TARGET_DATA', + ROLE_TARGET_MISSING_IDENTIFIER = 'ROLE_TARGET_MISSING_IDENTIFIER', + ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY = 'ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY', + ROLE_NOT_FOUND = 'ROLE_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role-targets.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role-targets.entity.ts index 0cd1c348e6..c16249910b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role-targets.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role-targets.entity.ts @@ -12,16 +12,18 @@ import { UpdateDateColumn, } from 'typeorm'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface'; + import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @Entity('roleTargets') -@Unique('IDX_ROLE_TARGETS_UNIQUE', [ +@Unique('IDX_ROLE_TARGETS_UNIQUE_USER_WORKSPACE', [ + 'workspaceId', 'userWorkspaceId', - 'roleId', - 'agentId', - 'apiKeyId', ]) +@Unique('IDX_ROLE_TARGETS_UNIQUE_AGENT', ['workspaceId', 'agentId']) +@Unique('IDX_ROLE_TARGETS_UNIQUE_API_KEY', ['workspaceId', 'apiKeyId']) @Index('IDX_ROLE_TARGETS_WORKSPACE_ID', ['userWorkspaceId', 'workspaceId']) @Index('IDX_ROLE_TARGETS_AGENT_ID', ['agentId']) @Index('IDX_ROLE_TARGETS_API_KEY_ID', ['apiKeyId']) @@ -29,7 +31,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; 'CHK_role_targets_single_entity', '("agentId" IS NOT NULL AND "userWorkspaceId" IS NULL AND "apiKeyId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NOT NULL AND "apiKeyId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NULL AND "apiKeyId" IS NOT NULL)', ) -export class RoleTargetsEntity { +export class RoleTargetsEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; @@ -46,13 +48,13 @@ export class RoleTargetsEntity { role: Relation; @Column({ nullable: true, type: 'uuid' }) - userWorkspaceId: string; + userWorkspaceId: string | null; @Column({ nullable: true, type: 'uuid' }) - agentId: string; + agentId: string | null; @Column({ nullable: true, type: 'uuid' }) - apiKeyId: string; + apiKeyId: string | null; @ManyToOne(() => ApiKeyEntity, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'apiKeyId' }) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index 1e4f2f2d51..8f6d29d5e3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -18,12 +18,12 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets @Entity('role') @Unique('IDX_ROLE_LABEL_WORKSPACE_ID_UNIQUE', ['label', 'workspaceId']) -export class RoleEntity extends SyncableEntity { +export class RoleEntity extends SyncableEntity implements Required { @PrimaryGeneratedColumn('uuid') id: string; @Column({ nullable: true, type: 'uuid' }) - standardId?: string; + standardId: string | null; @Column({ nullable: false }) label: string; @@ -47,10 +47,10 @@ export class RoleEntity extends SyncableEntity { canDestroyAllObjectRecords: boolean; @Column({ nullable: true, type: 'text' }) - description: string; + description: string | null; - @Column({ nullable: true }) - icon: string; + @Column({ nullable: true, type: 'varchar' }) + icon: string | null; @Column({ nullable: false, type: 'uuid' }) workspaceId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index 8ab90c6b31..01f0c1b159 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -6,34 +6,61 @@ import { ApplicationModule } from 'src/engine/core-modules/application/applicati import { FileModule } from 'src/engine/core-modules/file/file.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { WorkspaceFlatRoleTargetMapCacheService } from 'src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service'; +import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { WorkspaceFlatRoleMapCacheService } from 'src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; @Module({ imports: [ - TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity]), - TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]), + TypeOrmModule.forFeature([ + RoleEntity, + RoleTargetsEntity, + ObjectPermissionEntity, + PermissionFlagEntity, + FieldPermissionEntity, + UserWorkspaceEntity, + ]), UserRoleModule, AiAgentRoleModule, + ApplicationModule, ApiKeyModule, PermissionsModule, - UserWorkspaceModule, ObjectPermissionModule, PermissionFlagModule, WorkspacePermissionsCacheModule, + WorkspaceCacheStorageModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceMigrationV2Module, + UserWorkspaceModule, FileModule, - ApplicationModule, ], - providers: [RoleService, RoleResolver], - exports: [RoleService], + providers: [ + RoleService, + RoleResolver, + WorkspaceFlatRoleMapCacheService, + WorkspaceFlatRoleTargetMapCacheService, + WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + ], + exports: [ + RoleService, + WorkspaceFlatRoleMapCacheService, + WorkspaceFlatRoleTargetMapCacheService, + ], }) export class RoleModule {} 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 a332cc8550..832c7ee306 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 @@ -1,4 +1,9 @@ -import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; +import { + UseFilters, + UseGuards, + UseInterceptors, + UsePipes, +} from '@nestjs/common'; import { Args, Mutation, @@ -12,6 +17,7 @@ import { msg } from '@lingui/core/macro'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -49,7 +55,9 @@ import { } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { fromRoleEntitiesToRoleDtos } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Resolver(() => RoleDTO) @@ -62,6 +70,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta PermissionsGraphqlApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter, ) +@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) export class RoleResolver { constructor( private readonly userRoleService: UserRoleService, @@ -72,13 +81,16 @@ export class RoleResolver { private readonly agentRoleService: AiAgentRoleService, private readonly apiKeyRoleService: ApiKeyRoleService, private readonly fieldPermissionService: FieldPermissionService, + private readonly applicationService: ApplicationService, ) {} @Query(() => [RoleDTO]) async getRoles( @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - return this.roleService.getWorkspaceRoles(workspace.id); + const roleEntities = await this.roleService.getWorkspaceRoles(workspace.id); + + return fromRoleEntitiesToRoleDtos(roleEntities); } @Mutation(() => WorkspaceMemberDTO) @@ -141,9 +153,18 @@ export class RoleResolver { @AuthWorkspace() workspace: WorkspaceEntity, @Args('createRoleInput') createRoleInput: CreateRoleInput, ): Promise { + const { id: workspaceId } = workspace; + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + return await this.roleService.createRole({ - workspaceId: workspace.id, + workspaceId, input: createRoleInput, + applicationId: workspaceCustomFlatApplication.id, }); } @@ -165,12 +186,12 @@ export class RoleResolver { @AuthWorkspace() workspace: WorkspaceEntity, @Args('roleId', { type: () => UUIDScalarType }) roleId: string, ): Promise { - const deletedRoleId = await this.roleService.deleteRole( + const deletedRole = await this.roleService.deleteRole({ roleId, - workspace.id, - ); + workspaceId: workspace.id, + }); - return deletedRoleId; + return deletedRole.id; } @Mutation(() => [ObjectPermissionDTO]) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts index b3252f63a7..fe3dd11d12 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts @@ -1,12 +1,17 @@ +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { v4 } from 'uuid'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { fromCreateRoleInputToFlatRoleToCreate } from 'src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util'; +import { fromDeleteRoleInputToFlatRoleOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util'; +import { fromUpdateRoleInputToFlatRoleToUpdateOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util'; import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants'; import { PermissionsException, @@ -14,24 +19,24 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { type CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; -import { - type UpdateRoleInput, - type UpdateRolePayload, -} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; +import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; +import { type UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { fromFlatRoleToRoleDto } from 'src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; +@Injectable() export class RoleService { constructor( + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, @InjectRepository(RoleEntity) private readonly roleRepository: Repository, private readonly userRoleService: UserRoleService, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, - private readonly applicationService: ApplicationService, ) {} public async getWorkspaceRoles(workspaceId: string): Promise { @@ -69,46 +74,66 @@ export class RoleService { public async createRole({ input, workspaceId, + applicationId, }: { input: CreateRoleInput; workspaceId: string; - }): Promise { - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + applicationId: string; + }): Promise { + const { flatRoleMaps: existingFlatRoleMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, + flatMapsKeys: ['flatRoleMaps'], + }, + ); + + const flatRoleToCreate = fromCreateRoleInputToFlatRoleToCreate({ + createRoleInput: input, + workspaceId, + applicationId, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromToAllFlatEntityMaps: { + flatRoleMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRoleMaps, + flatEntityToCreate: [flatRoleToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), + }, + dependencyAllFlatEntityMaps: {}, + buildOptions: { + isSystemBuild: false, + }, + workspaceId, }, ); - await this.validateRoleInputOrThrow({ input, workspaceId }); + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while creating role', + ); + } - const id = input.id ?? v4(); - const role = await this.roleRepository.save({ - id, - label: input.label, - description: input.description, - icon: input.icon, - canUpdateAllSettings: input.canUpdateAllSettings, - canAccessAllTools: input.canAccessAllTools, - canReadAllObjectRecords: input.canReadAllObjectRecords, - canUpdateAllObjectRecords: input.canUpdateAllObjectRecords, - canSoftDeleteAllObjectRecords: input.canSoftDeleteAllObjectRecords, - canDestroyAllObjectRecords: input.canDestroyAllObjectRecords, - canBeAssignedToUsers: input.canBeAssignedToUsers, - canBeAssignedToAgents: input.canBeAssignedToAgents, - canBeAssignedToApiKeys: input.canBeAssignedToApiKeys, - isEditable: true, - workspaceId, - applicationId: workspaceCustomFlatApplication.id, - universalIdentifier: id, - }); + const { flatRoleMaps: recomputedFlatRoleMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleMaps'], + }, + ); - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - roleIds: [role.id], - }); - - return role; + return fromFlatRoleToRoleDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatRoleToCreate.id, + flatEntityMaps: recomputedFlatRoleMaps, + }), + ); } public async updateRole({ @@ -117,57 +142,69 @@ export class RoleService { }: { input: UpdateRoleInput; workspaceId: string; - }): Promise { - await this.validateRoleIsEditableOrThrow({ - roleId: input.id, - workspaceId, - }); - - const existingRole = await this.roleRepository.findOne({ - where: { - id: input.id, - workspaceId, - }, - }); - - if (!isDefined(existingRole)) { - throw new PermissionsException( - PermissionsExceptionMessage.ROLE_NOT_FOUND, - PermissionsExceptionCode.ROLE_NOT_FOUND, + }): Promise { + const { flatRoleMaps: existingFlatRoleMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { - userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`, + workspaceId, + flatMapsKeys: ['flatRoleMaps'], }, ); + + const flatRoleToUpdate = fromUpdateRoleInputToFlatRoleToUpdateOrThrow({ + flatRoleMaps: existingFlatRoleMaps, + updateRoleInput: input, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromToAllFlatEntityMaps: { + flatRoleMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRoleMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [flatRoleToUpdate], + }), + }, + dependencyAllFlatEntityMaps: {}, + buildOptions: { + isSystemBuild: false, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while updating role', + ); } - await this.validateRoleInputOrThrow({ - input: input.update, - workspaceId, - roleId: input.id, - }); + const { flatRoleMaps: recomputedFlatRoleMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleMaps'], + }, + ); - const updatedRole = await this.roleRepository.save({ - id: input.id, - ...input.update, - }); - - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - roleIds: [input.id], - }); - - return { ...existingRole, ...updatedRole }; + return fromFlatRoleToRoleDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: input.id, + flatEntityMaps: recomputedFlatRoleMaps, + }), + ); } - public async deleteRole( - roleId: string, - workspaceId: string, - ): Promise { - await this.validateRoleIsEditableOrThrow({ - roleId, - workspaceId, - }); - + public async deleteRole({ + roleId, + workspaceId, + }: { + roleId: string; + workspaceId: string; + }): Promise { const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId, @@ -186,10 +223,15 @@ export class RoleService { ); } - await this.validateRoleIsNotDefaultRoleOrThrow({ - roleId, - defaultRoleId, - }); + if (defaultRoleId === roleId) { + throw new PermissionsException( + PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED, + PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED, + { + userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`, + }, + ); + } await this.assignDefaultRoleToMembersWithRoleToDelete({ roleId, @@ -197,16 +239,49 @@ export class RoleService { defaultRoleId, }); - await this.roleRepository.delete({ - id: roleId, - workspaceId, + const { flatRoleMaps: existingFlatRoleMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRoleMaps'], + }, + ); + + const flatRoleToDelete = fromDeleteRoleInputToFlatRoleOrThrow({ + flatRoleMaps: existingFlatRoleMaps, + roleId, }); - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - }); + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + fromToAllFlatEntityMaps: { + flatRoleMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRoleMaps, + flatEntityToCreate: [], + flatEntityToDelete: [flatRoleToDelete], + flatEntityToUpdate: [], + }), + }, + dependencyAllFlatEntityMaps: {}, + buildOptions: { + inferDeletionFromMissingEntities: { + role: true, + }, + isSystemBuild: false, + }, + workspaceId, + }, + ); - return roleId; + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderExceptionV2( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting role', + ); + } + + return fromFlatRoleToRoleDto(flatRoleToDelete); } public async createMemberRole({ @@ -215,165 +290,55 @@ export class RoleService { }: { applicationId: string; workspaceId: string; - }): Promise { - const id = v4(); - - return this.roleRepository.save({ - label: MEMBER_ROLE_LABEL, - description: 'Member role', - icon: 'IconUser', - canUpdateAllSettings: false, - canAccessAllTools: true, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - canBeAssignedToUsers: true, - canBeAssignedToAgents: false, - canBeAssignedToApiKeys: false, - isEditable: true, - workspaceId, + }): Promise { + return this.createRole({ + input: { + label: MEMBER_ROLE_LABEL, + description: 'Member role', + icon: 'IconUser', + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, applicationId, - id, - universalIdentifier: id, + workspaceId, }); } - // Only used for dev seeding and testing public async createGuestRole({ workspaceId, applicationId, }: { workspaceId: string; applicationId: string; - }): Promise { - const id = v4(); - - return this.roleRepository.save({ - label: 'Guest', - description: 'Guest role', - icon: 'IconUser', - canUpdateAllSettings: false, - canAccessAllTools: false, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - canBeAssignedToUsers: true, - canBeAssignedToAgents: false, - canBeAssignedToApiKeys: false, - isEditable: false, + }): Promise { + return this.createRole({ + input: { + label: 'Guest', + description: 'Guest role', + icon: 'IconUser', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, workspaceId, applicationId, - id, - universalIdentifier: id, }); } - private async validateRoleInputOrThrow({ - input, - workspaceId, - roleId, - }: { - input: CreateRoleInput | UpdateRolePayload; - workspaceId: string; - roleId?: string; - }): Promise { - const keysToValidate = [ - 'label', - 'canUpdateAllSettings', - 'canAccessAllTools', - 'canReadAllObjectRecords', - 'canUpdateAllObjectRecords', - 'canSoftDeleteAllObjectRecords', - 'canDestroyAllObjectRecords', - ]; - - for (const key of keysToValidate) { - try { - isArgDefinedIfProvidedOrThrow({ - input, - key, - // @ts-expect-error legacy noImplicitAny - value: input[key], - }); - } catch (error) { - throw new PermissionsException( - error.message, - PermissionsExceptionCode.INVALID_ARG, - { - userFriendlyMessage: msg`Some of the information provided is invalid. Please check your input and try again.`, - }, - ); - } - } - - const workspaceRoles = await this.getWorkspaceRoles(workspaceId); - - if (isDefined(input.label)) { - let rolesForLabelComparison = workspaceRoles; - - if (isDefined(roleId)) { - rolesForLabelComparison = workspaceRoles.filter( - (role) => role.id !== roleId, - ); - } - - if (rolesForLabelComparison.some((role) => role.label === input.label)) { - throw new PermissionsException( - PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS, - PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS, - { userFriendlyMessage: msg`A role with this label already exists.` }, - ); - } - } - - const existingRole = workspaceRoles.find((role) => role.id === roleId); - - await this.validateRoleReadAndWirtePermissionsConsistencyOrThrow({ - input, - existingRole, - }); - } - - private async validateRoleReadAndWirtePermissionsConsistencyOrThrow({ - input, - existingRole, - }: { - input: CreateRoleInput | UpdateRolePayload; - existingRole?: RoleEntity; - }) { - const hasReadingPermissionsAfterUpdate = - input.canReadAllObjectRecords ?? existingRole?.canReadAllObjectRecords; - - const hasUpdatePermissionsAfterUpdate = - input.canUpdateAllObjectRecords ?? - existingRole?.canUpdateAllObjectRecords; - - const hasSoftDeletePermissionsAfterUpdate = - input.canSoftDeleteAllObjectRecords ?? - existingRole?.canSoftDeleteAllObjectRecords; - - const hasDestroyPermissionsAfterUpdate = - input.canDestroyAllObjectRecords ?? - existingRole?.canDestroyAllObjectRecords; - - if ( - hasReadingPermissionsAfterUpdate === false && - (hasUpdatePermissionsAfterUpdate || - hasSoftDeletePermissionsAfterUpdate || - hasDestroyPermissionsAfterUpdate) - ) { - throw new PermissionsException( - PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION, - PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION, - { - userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`, - }, - ); - } - } - + // TODO: Move to migration side effect / To address for rollback of role deletion private async assignDefaultRoleToMembersWithRoleToDelete({ roleId, workspaceId, @@ -399,54 +364,4 @@ export class RoleService { ), ); } - - private async getRole( - roleId: string, - workspaceId: string, - ): Promise { - return this.roleRepository.findOne({ - where: { - id: roleId, - workspaceId, - }, - }); - } - - private async validateRoleIsEditableOrThrow({ - roleId, - workspaceId, - }: { - roleId: string; - workspaceId: string; - }) { - const role = await this.getRole(roleId, workspaceId); - - if (!role?.isEditable) { - throw new PermissionsException( - PermissionsExceptionMessage.ROLE_NOT_EDITABLE, - PermissionsExceptionCode.ROLE_NOT_EDITABLE, - { - userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`, - }, - ); - } - } - - private async validateRoleIsNotDefaultRoleOrThrow({ - roleId, - defaultRoleId, - }: { - roleId: string; - defaultRoleId: string; - }): Promise { - if (defaultRoleId === roleId) { - throw new PermissionsException( - PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED, - PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED, - { - userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`, - }, - ); - } - } } 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 new file mode 100644 index 0000000000..34da186541 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts @@ -0,0 +1,128 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util'; +import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; +import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; + +@Injectable() +@WorkspaceFlatMapCache('flatRoleMaps') +export class WorkspaceFlatRoleMapCacheService extends WorkspaceFlatMapCacheService< + FlatEntityMaps +> { + constructor( + @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) + cacheStorageService: CacheStorageService, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + @InjectRepository(RoleTargetsEntity) + private readonly roleTargetsRepository: Repository, + @InjectRepository(ObjectPermissionEntity) + private readonly objectPermissionRepository: Repository, + @InjectRepository(PermissionFlagEntity) + private readonly permissionFlagRepository: Repository, + @InjectRepository(FieldPermissionEntity) + private readonly fieldPermissionRepository: Repository, + ) { + super(cacheStorageService); + } + + protected async computeFlatMap({ + workspaceId, + }: { + workspaceId: string; + }): Promise> { + const [ + roles, + roleTargets, + objectPermissions, + permissionFlags, + fieldPermissions, + ] = await Promise.all([ + this.roleRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.roleTargetsRepository.find({ + where: { workspaceId }, + select: ['id', 'roleId'], + withDeleted: true, + }), + this.objectPermissionRepository.find({ + where: { workspaceId }, + select: ['id', 'roleId'], + withDeleted: true, + }), + this.permissionFlagRepository.find({ + where: { workspaceId }, + select: ['id', 'roleId'], + withDeleted: true, + }), + this.fieldPermissionRepository.find({ + where: { workspaceId }, + select: ['id', 'roleId'], + withDeleted: true, + }), + ]); + + const [ + roleTargetsByRoleId, + objectPermissionsByRoleId, + permissionFlagsByRoleId, + fieldPermissionsByRoleId, + ] = ( + [ + { + entities: roleTargets, + foreignKey: 'roleId', + }, + { + entities: objectPermissions, + foreignKey: 'roleId', + }, + { + entities: permissionFlags, + foreignKey: 'roleId', + }, + { + entities: fieldPermissions, + foreignKey: 'roleId', + }, + ] as const + ).map(regroupEntitiesByRelatedEntityId); + + const flatRoleMaps = createEmptyFlatEntityMaps(); + + for (const roleEntity of roles) { + const flatRole = fromRoleEntityToFlatRole({ + ...roleEntity, + roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [], + objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [], + permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [], + fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [], + } as RoleEntity); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatRole, + flatEntityMapsToMutate: flatRoleMaps, + }); + } + + return flatRoleMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util.ts new file mode 100644 index 0000000000..48cc6aa653 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util.ts @@ -0,0 +1,38 @@ +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; + +export const fromFlatRoleToRoleDto = ({ + canAccessAllTools, + canBeAssignedToAgents, + canBeAssignedToApiKeys, + canBeAssignedToUsers, + canDestroyAllObjectRecords, + canReadAllObjectRecords, + canSoftDeleteAllObjectRecords, + canUpdateAllObjectRecords, + canUpdateAllSettings, + id, + isEditable, + label, + description, + icon, + standardId, +}: FlatRole): RoleDTO => { + return { + canAccessAllTools, + canBeAssignedToAgents, + canBeAssignedToApiKeys, + canBeAssignedToUsers, + canDestroyAllObjectRecords, + canReadAllObjectRecords, + canSoftDeleteAllObjectRecords, + canUpdateAllObjectRecords, + canUpdateAllSettings, + id, + isEditable, + label, + description: description ?? undefined, + icon: icon ?? undefined, + standardId: standardId ?? undefined, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts index 95b43dd223..a72f4268a8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts @@ -4,12 +4,12 @@ import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => { return { id: role.id, - standardId: role.standardId, + standardId: role.standardId ?? undefined, label: role.label, canUpdateAllSettings: role.canUpdateAllSettings, canAccessAllTools: role.canAccessAllTools, - description: role.description, - icon: role.icon, + description: role.description ?? undefined, + icon: role.icon ?? undefined, isEditable: role.isEditable, canReadAllObjectRecords: role.canReadAllObjectRecords, canUpdateAllObjectRecords: role.canUpdateAllObjectRecords, @@ -19,6 +19,9 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => { canBeAssignedToAgents: role.canBeAssignedToAgents, canBeAssignedToApiKeys: role.canBeAssignedToApiKeys, roleTargets: role.roleTargets, + permissionFlags: role.permissionFlags, + objectPermissions: role.objectPermissions, + fieldPermissions: role.fieldPermissions, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts index 275302ca74..15487c8ee7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; @@ -12,6 +13,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity]), TypeOrmModule.forFeature([UserWorkspaceEntity]), WorkspacePermissionsCacheModule, + RoleTargetModule, ], providers: [UserRoleService], exports: [UserRoleService], diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index 4c89303d89..ef166110f6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; -import { type QueryRunner, In, Not, Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { @@ -10,6 +10,7 @@ import { PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @@ -19,28 +20,24 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta export class UserRoleService { constructor( - @InjectRepository(RoleEntity) - private readonly roleRepository: Repository, @InjectRepository(RoleTargetsEntity) private readonly roleTargetsRepository: Repository, @InjectRepository(UserWorkspaceEntity) private readonly userWorkspaceRepository: Repository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly roleTargetService: RoleTargetService, ) {} - public async assignRoleToUserWorkspace( - { - workspaceId, - userWorkspaceId, - roleId, - }: { - workspaceId: string; - userWorkspaceId: string; - roleId: string; - }, - queryRunner?: QueryRunner, - ): Promise { + public async assignRoleToUserWorkspace({ + workspaceId, + userWorkspaceId, + roleId, + }: { + workspaceId: string; + userWorkspaceId: string; + roleId: string; + }): Promise { const validationResult = await this.validateAssignRoleInput({ userWorkspaceId, workspaceId, @@ -51,27 +48,14 @@ export class UserRoleService { return; } - const roleTargetsRepo = queryRunner - ? queryRunner.manager.getRepository(RoleTargetsEntity) - : this.roleTargetsRepository; - - const newRoleTarget = await roleTargetsRepo.save({ - roleId, - userWorkspaceId, - workspaceId, - }); - - await roleTargetsRepo.delete({ - userWorkspaceId, - workspaceId, - id: Not(newRoleTarget.id), - }); - - await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache( - { - workspaceId, + await this.roleTargetService.create({ + createRoleTargetInput: { + roleId, + targetId: userWorkspaceId, + targetMetadataForeignKey: 'userWorkspaceId', }, - ); + workspaceId, + }); } public async getRoleIdForUserWorkspace({ @@ -248,32 +232,6 @@ export class UserRoleService { ); } - const role = await this.roleRepository.findOne({ - where: { - id: roleId, - }, - }); - - if (!isDefined(role)) { - throw new PermissionsException( - 'Role not found', - PermissionsExceptionCode.ROLE_NOT_FOUND, - { - userFriendlyMessage: msg`The role you are trying to assign could not be found. It may have been deleted.`, - }, - ); - } - - if (!role.canBeAssignedToUsers) { - throw new PermissionsException( - `Role "${role.label}" cannot be assigned to users`, - PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS, - { - userFriendlyMessage: msg`This role cannot be assigned to users. Please select a different role.`, - }, - ); - } - const roles = await this.getRolesByUserWorkspaces({ userWorkspaceIds: [userWorkspace.id], workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index d30da2b8c9..2f0b713366 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Record } from 'cloudflare/core'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { type ObjectsPermissions, type ObjectsPermissionsByRoleId, @@ -9,7 +10,6 @@ import { } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In, IsNull, Not, Repository } from 'typeorm'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; @@ -290,9 +290,9 @@ export class WorkspacePermissionsCacheService { canDestroyObjectRecords: canDestroy, restrictedFields, }; - - permissionsByRoleId[role.id] = objectRecordsPermissions; } + + permissionsByRoleId[role.id] = objectRecordsPermissions; } return permissionsByRoleId; @@ -328,11 +328,15 @@ export class WorkspacePermissionsCacheService { }, }); - return roleTargetsMap.reduce((acc, roleTarget) => { - acc[roleTarget.userWorkspaceId] = roleTarget.roleId; + const initialAccumulator: UserWorkspaceRoleMap = {}; - return acc; - }, {} as UserWorkspaceRoleMap); + return roleTargetsMap.reduce( + (acc, roleTarget) => ({ + ...acc, + [roleTarget.userWorkspaceId as string]: roleTarget.roleId, + }), + initialAccumulator, + ); } private hasWorkflowsPermissions(role: RoleEntity): boolean { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts index 515338ccc7..452d5a1fee 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts @@ -9,6 +9,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; 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 { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; @@ -38,6 +39,7 @@ export class DevSeederPermissionsService { private readonly roleRepository: Repository, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly fieldPermissionService: FieldPermissionService, + private readonly roleTargetService: RoleTargetService, @InjectDataSource() private readonly coreDataSource: DataSource, ) {} @@ -63,28 +65,15 @@ export class DevSeederPermissionsService { } try { - await this.coreDataSource - .createQueryBuilder() - .insert() - .into('core.roleTargets', ['roleId', 'apiKeyId', 'workspaceId']) - .orIgnore() - .values([ - { - roleId: adminRole.id, - apiKeyId: API_KEY_DATA_SEED_IDS.ID_1, - workspaceId: workspaceId, - }, - ]) - .execute(); - - await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({ + await this.roleTargetService.create({ + createRoleTargetInput: { + roleId: adminRole.id, + targetId: API_KEY_DATA_SEED_IDS.ID_1, + targetMetadataForeignKey: 'apiKeyId', + applicationId: twentyStandardApplication.id, + }, workspaceId, }); - await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache( - { - workspaceId, - }, - ); } catch (error) { this.logger.error( `Could not assign role to test API key: ${error.message}`, @@ -116,8 +105,10 @@ export class DevSeederPermissionsService { roleId: guestRole.id, }); - const limitedRole = - await this.createLimitedRoleForSeedWorkspace(workspaceId); + const limitedRole = await this.createLimitedRoleForSeedWorkspace({ + workspaceId, + applicationId: twentyStandardApplication.id, + }); await this.userRoleService.assignRoleToUserWorkspace({ workspaceId, @@ -164,8 +155,15 @@ export class DevSeederPermissionsService { } } - private async createLimitedRoleForSeedWorkspace(workspaceId: string) { + private async createLimitedRoleForSeedWorkspace({ + applicationId, + workspaceId, + }: { + workspaceId: string; + applicationId: string; + }) { const customRole = await this.roleService.createRole({ + applicationId, workspaceId, input: { label: 'Object-restricted', diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts index beb9839f0b..ab0dffee93 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts @@ -13,6 +13,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module'; +import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module'; import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @@ -34,6 +35,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp TypeORMModule, DataSourceModule, RoleModule, + RoleTargetModule, UserRoleModule, ApiKeyModule, ApplicationModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-metadata-validation-response-error.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-metadata-validation-response-error.util.ts index d921f02fef..dd15c6ee4e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-metadata-validation-response-error.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-metadata-validation-response-error.util.ts @@ -28,6 +28,8 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE invalidRouteTrigger: 0, invalidFieldMetadata: 0, invalidViewGroup: 0, + invalidRole: 0, + invalidRoleTarget: 0, totalErrors: 0, }, errors: translatedReport, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts new file mode 100644 index 0000000000..7943316c86 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts @@ -0,0 +1,38 @@ +import { + type CallHandler, + type ExecutionContext, + Injectable, + type NestInterceptor, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import { type Observable, catchError } from 'rxjs'; +import { SOURCE_LOCALE } from 'twenty-shared/translations'; + +import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; +import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter'; + +@Injectable() +export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor + implements NestInterceptor +{ + constructor(private readonly i18nService: I18nService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const gqlContext = GqlExecutionContext.create(context); + const ctx = gqlContext.getContext(); + const locale = ctx.req?.locale ?? SOURCE_LOCALE; + const i18n = this.i18nService.getI18nInstance(locale); + + return next.handle().pipe( + catchError((error) => { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + workspaceMigrationBuilderExceptionV2Formatter(error, i18n); + } + + throw error; + }), + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts index b7be171a25..7b5226536d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts @@ -19,6 +19,8 @@ import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 's import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service'; import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service'; import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service'; +import { WorkspaceMigrationV2RoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service'; +import { WorkspaceMigrationV2RoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service'; import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service'; import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service'; import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service'; @@ -39,7 +41,9 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationV2DatabaseEventTriggerActionsBuilderService: WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService, private readonly workspaceMigrationV2CronTriggerActionsBuilderService: WorkspaceMigrationV2CronTriggerActionsBuilderService, private readonly workspaceMigrationV2RouteTriggerActionsBuilderService: WorkspaceMigrationV2RouteTriggerActionsBuilderService, + private readonly workspaceMigrationV2RoleTargetActionsBuilderService: WorkspaceMigrationV2RoleTargetActionsBuilderService, private readonly workspaceMigrationV2FieldActionsBuilderService: WorkspaceMigrationV2FieldActionsBuilderService, + private readonly workspaceMigrationV2RoleActionsBuilderService: WorkspaceMigrationV2RoleActionsBuilderService, ) {} private setupOptimisticCache({ @@ -125,6 +129,8 @@ export class WorkspaceMigrationBuildOrchestratorService { flatFieldMetadataMaps, flatViewFilterMaps, flatViewGroupMaps, + flatRoleMaps, + flatRoleTargetMaps, } = fromToAllFlatEntityMaps; if (isDefined(flatObjectMetadataMaps)) { @@ -513,6 +519,65 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatRoleMaps)) { + const { from: fromFlatRoleMaps, to: toFlatRoleMaps } = flatRoleMaps; + + const roleResult = + this.workspaceMigrationV2RoleActionsBuilderService.validateAndBuild({ + from: fromFlatRoleMaps, + to: toFlatRoleMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: undefined, + workspaceId, + }); + + this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( + { + allFlatEntityMaps: optimisticAllFlatEntityMaps, + flatEntityMapsAndRelatedFlatEntityMaps: + roleResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, + }, + ); + + if (roleResult.status === 'fail') { + orchestratorFailureReport.role.push(...roleResult.errors); + } else { + orchestratorActionsReport.role = roleResult.actions; + } + } + + if (isDefined(flatRoleTargetMaps)) { + const { from: fromFlatRoleTargetMaps, to: toFlatRoleTargetMaps } = + flatRoleTargetMaps; + + const roleTargetResult = + this.workspaceMigrationV2RoleTargetActionsBuilderService.validateAndBuild( + { + from: fromFlatRoleTargetMaps, + to: toFlatRoleTargetMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: { + flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps, + }, + workspaceId, + }, + ); + + this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( + { + allFlatEntityMaps: optimisticAllFlatEntityMaps, + flatEntityMapsAndRelatedFlatEntityMaps: + roleTargetResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, + }, + ); + + if (roleTargetResult.status === 'fail') { + orchestratorFailureReport.roleTarget.push(...roleTargetResult.errors); + } else { + orchestratorActionsReport.roleTarget = roleTargetResult.actions; + } + } + const allErrors = Object.values(orchestratorFailureReport); if (allErrors.some((report) => report.length > 0)) { @@ -585,9 +650,21 @@ export class WorkspaceMigrationBuildOrchestratorService { /// // Route triggers - ...orchestratorActionsReport.routeTrigger.deleted, - ...orchestratorActionsReport.routeTrigger.created, - ...orchestratorActionsReport.routeTrigger.updated, + ...aggregatedOrchestratorActionsReport.routeTrigger.deleted, + ...aggregatedOrchestratorActionsReport.routeTrigger.created, + ...aggregatedOrchestratorActionsReport.routeTrigger.updated, + /// + + // Roles + ...aggregatedOrchestratorActionsReport.role.deleted, + ...aggregatedOrchestratorActionsReport.role.created, + ...aggregatedOrchestratorActionsReport.role.updated, + /// + + // Role targets + ...aggregatedOrchestratorActionsReport.roleTarget.deleted, + ...aggregatedOrchestratorActionsReport.roleTarget.created, + ...aggregatedOrchestratorActionsReport.roleTarget.updated, /// ], workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type.ts new file mode 100644 index 0000000000..9d821ac41d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type.ts @@ -0,0 +1,26 @@ +import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; + +export type CreateRoleTargetAction = { + type: 'create_role_target'; + roleTarget: FlatRoleTarget; +}; + +export type UpdateRoleTargetAction = { + type: 'update_role_target'; + roleTargetId: string; + updates: FlatEntityPropertiesUpdates<'roleTarget'>; +}; + +export type DeleteRoleTargetAction = { + type: 'delete_role_target'; + roleTargetId: string; +}; + +export type WorkspaceMigrationRoleTargetActionV2 = + | CreateRoleTargetAction + | UpdateRoleTargetAction + | DeleteRoleTargetAction; + +export type WorkspaceMigrationRoleTargetActionTypeV2 = + WorkspaceMigrationRoleTargetActionV2['type']; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service.ts new file mode 100644 index 0000000000..1e4ac1ee90 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { UpdateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type'; +import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; +import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; +import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type'; +import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service'; + +@Injectable() +export class WorkspaceMigrationV2RoleTargetActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service< + typeof ALL_METADATA_NAME.roleTarget +> { + constructor( + private readonly flatRoleTargetValidatorService: FlatRoleTargetValidatorService, + ) { + super(ALL_METADATA_NAME.roleTarget); + } + + protected validateFlatEntityCreation( + args: FlatEntityValidationArgs, + ): FlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.roleTarget, + 'created' + > { + const validationResult = + this.flatRoleTargetValidatorService.validateFlatRoleTargetCreation(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatRoleTargetToValidate } = args; + + return { + status: 'success', + action: { + type: 'create_role_target', + roleTarget: flatRoleTargetToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: FlatEntityValidationArgs, + ): FlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.roleTarget, + 'deleted' + > { + const validationResult = + this.flatRoleTargetValidatorService.validateFlatRoleTargetDeletion(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatRoleTargetToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete_role_target', + roleTargetId: flatRoleTargetToValidate.id, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs, + ): FlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.roleTarget, + 'updated' + > { + const validationResult = + this.flatRoleTargetValidatorService.validateFlatRoleTargetUpdate(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityId, flatEntityUpdates } = args; + + const updateRoleTargetAction: UpdateRoleTargetAction = { + type: 'update_role_target', + roleTargetId: flatEntityId, + updates: flatEntityUpdates, + }; + + return { + status: 'success', + action: updateRoleTargetAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type.ts new file mode 100644 index 0000000000..426269235e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type.ts @@ -0,0 +1,26 @@ +import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type'; +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; + +export type CreateRoleAction = { + type: 'create_role'; + role: FlatRole; +}; + +export type UpdateRoleAction = { + type: 'update_role'; + roleId: string; + updates: FlatEntityPropertiesUpdates<'role'>; +}; + +export type DeleteRoleAction = { + type: 'delete_role'; + roleId: string; +}; + +export type WorkspaceMigrationRoleActionV2 = + | CreateRoleAction + | UpdateRoleAction + | DeleteRoleAction; + +export type WorkspaceMigrationRoleActionTypeV2 = + WorkspaceMigrationRoleActionV2['type']; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service.ts new file mode 100644 index 0000000000..bc31e28919 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { UpdateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type'; +import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; +import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; +import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type'; +import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service'; + +@Injectable() +export class WorkspaceMigrationV2RoleActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service< + typeof ALL_METADATA_NAME.role +> { + constructor( + private readonly flatRoleValidatorService: FlatRoleValidatorService, + ) { + super(ALL_METADATA_NAME.role); + } + + protected validateFlatEntityCreation( + args: FlatEntityValidationArgs, + ): FlatEntityValidationReturnType { + const validationResult = + this.flatRoleValidatorService.validateFlatRoleCreation(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatRoleToValidate } = args; + + return { + status: 'success', + action: { + type: 'create_role', + role: flatRoleToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: FlatEntityValidationArgs, + ): FlatEntityValidationReturnType { + const validationResult = + this.flatRoleValidatorService.validateFlatRoleDeletion(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatRoleToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete_role', + roleId: flatRoleToValidate.id, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs, + ): FlatEntityValidationReturnType { + const validationResult = + this.flatRoleValidatorService.validateFlatRoleUpdate(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityId, flatEntityUpdates } = args; + + const updateRoleAction: UpdateRoleAction = { + type: 'update_role', + roleId: flatEntityId, + updates: flatEntityUpdates, + }; + + return { + status: 'success', + action: updateRoleAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service.ts new file mode 100644 index 0000000000..324cfda86e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service.ts @@ -0,0 +1,179 @@ +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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception'; +import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; +import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; +import { validateFlatRoleTargetAssignationAvailability } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util'; +import { validateFlatRoleTargetTargetsOnlyOneEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util'; +import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; + +@Injectable() +export class FlatRoleTargetValidatorService { + validateFlatRoleTargetCreation({ + flatEntityToValidate: flatRoleTargetToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatRoleTargetMaps: optimisticFlatRoleTargetMaps, + flatRoleMaps, + }, + }: FlatEntityValidationArgs< + typeof ALL_METADATA_NAME.roleTarget + >): FailedFlatEntityValidation { + const validationResult: FailedFlatEntityValidation = { + type: 'create_role_target', + errors: [], + flatEntityMinimalInformation: { + id: flatRoleTargetToValidate.id, + }, + }; + + const existingRoleTarget = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatRoleTargetToValidate.id, + flatEntityMaps: optimisticFlatRoleTargetMaps, + }); + + if (isDefined(existingRoleTarget)) { + validationResult.errors.push({ + code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA, + message: t`Role target with this id already exists`, + userFriendlyMessage: msg`Role target with this id already exists`, + }); + } + + validationResult.errors.push( + ...validateFlatRoleTargetTargetsOnlyOneEntity({ + flatRoleTarget: flatRoleTargetToValidate, + }), + ); + + const referencedRole = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatRoleTargetToValidate.roleId, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(referencedRole)) { + validationResult.errors.push({ + code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + } else { + validationResult.errors.push( + ...validateFlatRoleTargetAssignationAvailability({ + flatRole: referencedRole, + flatRoleTarget: flatRoleTargetToValidate, + }), + ); + } + + return validationResult; + } + + validateFlatRoleTargetDeletion({ + flatEntityToValidate: flatRoleTargetToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatRoleTargetMaps: optimisticFlatRoleTargetMaps, + }, + }: FlatEntityValidationArgs< + typeof ALL_METADATA_NAME.roleTarget + >): FailedFlatEntityValidation { + const validationResult: FailedFlatEntityValidation = { + type: 'delete_role_target', + errors: [], + flatEntityMinimalInformation: { + id: flatRoleTargetToValidate.id, + }, + }; + + const existingRoleTarget = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatRoleTargetToValidate.id, + flatEntityMaps: optimisticFlatRoleTargetMaps, + }); + + if (!isDefined(existingRoleTarget)) { + validationResult.errors.push({ + code: RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND, + message: t`Role target not found`, + userFriendlyMessage: msg`Role target not found`, + }); + + return validationResult; + } + + return validationResult; + } + + validateFlatRoleTargetUpdate({ + flatEntityId, + flatEntityUpdates, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatRoleTargetMaps: optimisticFlatRoleTargetMaps, + flatRoleMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.roleTarget + >): FailedFlatEntityValidation { + const validationResult: FailedFlatEntityValidation = { + type: 'update_role_target', + errors: [], + flatEntityMinimalInformation: { + id: flatEntityId, + }, + }; + + const existingRoleTarget = optimisticFlatRoleTargetMaps.byId[flatEntityId]; + + if (!isDefined(existingRoleTarget)) { + validationResult.errors.push({ + code: RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND, + message: t`Role target not found`, + userFriendlyMessage: msg`Role target not found`, + }); + + return validationResult; + } + + const updatedFlatRoleTarget = { + ...existingRoleTarget, + ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({ + updates: flatEntityUpdates, + }), + }; + + validationResult.errors.push( + ...validateFlatRoleTargetTargetsOnlyOneEntity({ + flatRoleTarget: updatedFlatRoleTarget, + }), + ); + + const referencedRole = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: updatedFlatRoleTarget.roleId, + flatEntityMaps: flatRoleMaps, + }); + + if (!isDefined(referencedRole)) { + validationResult.errors.push({ + code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + + return validationResult; + } + + validationResult.errors.push( + ...validateFlatRoleTargetAssignationAvailability({ + flatRole: referencedRole, + flatRoleTarget: updatedFlatRoleTarget, + }), + ); + + return validationResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service.ts new file mode 100644 index 0000000000..5dd96cfc47 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service.ts @@ -0,0 +1,176 @@ +import { Injectable } from '@nestjs/common'; + +import { msg, t } from '@lingui/core/macro'; +import { type ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util'; +import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; +import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; +import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; +import { validateRoleIsEditable } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util'; +import { validateRoleLabelUniqueness } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util'; +import { validateRoleReadWritePermissionsConsistency } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util'; +import { validateRoleRequiredPropertiesAreDefined } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util'; +import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; + +@Injectable() +export class FlatRoleValidatorService { + public validateFlatRoleCreation({ + flatEntityToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatRoleMaps: optimisticFlatRoleMaps, + }, + }: FlatEntityValidationArgs< + typeof ALL_METADATA_NAME.role + >): FailedFlatEntityValidation { + const validationResult: FailedFlatEntityValidation = { + type: 'create_role', + errors: [], + flatEntityMinimalInformation: { + id: flatEntityToValidate.id, + label: flatEntityToValidate.label, + }, + }; + + const existingRoles = Object.values(optimisticFlatRoleMaps.byId).filter( + isDefined, + ); + + validationResult.errors.push( + ...validateRoleRequiredPropertiesAreDefined({ + flatRole: flatEntityToValidate, + }), + ); + + validationResult.errors.push( + ...validateRoleLabelUniqueness({ + label: flatEntityToValidate.label, + existingFlatRoles: existingRoles, + }), + ); + + validationResult.errors.push( + ...validateRoleReadWritePermissionsConsistency({ + flatRole: flatEntityToValidate, + }), + ); + + return validationResult; + } + + public validateFlatRoleDeletion({ + flatEntityToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatRoleMaps: optimisticFlatRoleMaps, + }, + }: FlatEntityValidationArgs< + typeof ALL_METADATA_NAME.role + >): FailedFlatEntityValidation { + const validationResult: FailedFlatEntityValidation = { + type: 'delete_role', + errors: [], + flatEntityMinimalInformation: { + id: flatEntityToValidate.id, + label: flatEntityToValidate.label, + }, + }; + + const existingRole = optimisticFlatRoleMaps.byId[flatEntityToValidate.id]; + + if (!isDefined(existingRole)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_FOUND, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + + return validationResult; + } + + validationResult.errors.push( + ...validateRoleIsEditable({ + flatRole: existingRole, + }), + ); + + return validationResult; + } + + public validateFlatRoleUpdate({ + flatEntityId, + flatEntityUpdates, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatRoleMaps: optimisticFlatRoleMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.role + >): FailedFlatEntityValidation { + const validationResult: FailedFlatEntityValidation = { + type: 'update_role', + errors: [], + flatEntityMinimalInformation: { + id: flatEntityId, + }, + }; + + const fromFlatRole = optimisticFlatRoleMaps.byId[flatEntityId]; + + if (!isDefined(fromFlatRole)) { + validationResult.errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_FOUND, + message: t`Role not found`, + userFriendlyMessage: msg`Role not found`, + }); + + return validationResult; + } + + validationResult.errors.push( + ...validateRoleIsEditable({ + flatRole: fromFlatRole, + }), + ); + + const toFlatRole = { + ...fromFlatRole, + ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({ + updates: flatEntityUpdates, + }), + }; + + validationResult.errors.push( + ...validateRoleRequiredPropertiesAreDefined({ + flatRole: toFlatRole, + }), + ); + + const flatRoleLabelUpdate = findFlatEntityPropertyUpdate({ + flatEntityUpdates, + property: 'label', + }); + + if (isDefined(flatRoleLabelUpdate)) { + const existingRoles = Object.values(optimisticFlatRoleMaps.byId).filter( + isDefined, + ); + + validationResult.errors.push( + ...validateRoleLabelUniqueness({ + label: flatRoleLabelUpdate.to, + existingFlatRoles: existingRoles, + }), + ); + } + + validationResult.errors.push( + ...validateRoleReadWritePermissionsConsistency({ + flatRole: toFlatRole, + }), + ); + + return validationResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util.ts new file mode 100644 index 0000000000..9382ccee76 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util.ts @@ -0,0 +1,48 @@ +import { msg, t } from '@lingui/core/macro'; +import { isDefined } from 'class-validator'; + +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'; +import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; + +type ValidateFlatRoleTargetAssignationAvailabilityArgs = { + flatRole: FlatRole; + flatRoleTarget: FlatRoleTarget; +}; +export const validateFlatRoleTargetAssignationAvailability = ({ + flatRole, + flatRoleTarget, +}: ValidateFlatRoleTargetAssignationAvailabilityArgs): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + const roleLabel = flatRole.label; + + if (isDefined(flatRoleTarget.agentId)) { + if (!flatRole.canBeAssignedToAgents) { + errors.push({ + code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY, + message: t`Role "${roleLabel}" cannot be assigned to agents`, + userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to agents`, + }); + } + } else if (isDefined(flatRoleTarget.userWorkspaceId)) { + if (!flatRole.canBeAssignedToUsers) { + errors.push({ + code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY, + message: t`Role "${roleLabel}" cannot be assigned to users`, + userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to users`, + }); + } + } else if (isDefined(flatRoleTarget.apiKeyId)) { + if (!flatRole.canBeAssignedToApiKeys) { + errors.push({ + code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY, + message: t`Role "${roleLabel}" cannot be assigned to API keys`, + userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to API keys`, + }); + } + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util.ts new file mode 100644 index 0000000000..9ce0356665 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util.ts @@ -0,0 +1,30 @@ +import { msg, t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; +import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; + +export const validateFlatRoleTargetTargetsOnlyOneEntity = ({ + flatRoleTarget, +}: { + flatRoleTarget: FlatRoleTarget; +}) => { + const errors: FlatEntityValidationError[] = []; + + const definedIdentifiersCount = [ + isDefined(flatRoleTarget.apiKeyId), + isDefined(flatRoleTarget.userWorkspaceId), + isDefined(flatRoleTarget.agentId), + ].filter(Boolean).length; + + if (definedIdentifiersCount !== 1) { + errors.push({ + code: RoleTargetExceptionCode.ROLE_TARGET_MISSING_IDENTIFIER, + message: t`Role target must have exactly one of: apiKeyId, userWorkspaceId, or agentId`, + userFriendlyMessage: msg`Role target must have exactly one of: apiKeyId, userWorkspaceId, or agentId`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util.ts new file mode 100644 index 0000000000..2e3b99cc50 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util.ts @@ -0,0 +1,26 @@ +import { msg } from '@lingui/core/macro'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; + +export const validateRoleIsEditable = ({ + flatRole, +}: { + flatRole: FlatRole; +}): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + if (!flatRole.isEditable) { + errors.push({ + code: PermissionsExceptionCode.ROLE_NOT_EDITABLE, + message: PermissionsExceptionMessage.ROLE_NOT_EDITABLE, + userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util.ts new file mode 100644 index 0000000000..8db572b898 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util.ts @@ -0,0 +1,28 @@ +import { msg } from '@lingui/core/macro'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; + +export const validateRoleLabelUniqueness = ({ + label, + existingFlatRoles, +}: { + label: string; + existingFlatRoles: FlatRole[]; +}): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + if (existingFlatRoles.some((role) => role.label === label)) { + errors.push({ + code: PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS, + message: PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS, + userFriendlyMessage: msg`A role with this label already exists.`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util.ts new file mode 100644 index 0000000000..e3c86016f4 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util.ts @@ -0,0 +1,32 @@ +import { msg } from '@lingui/core/macro'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; + +export const validateRoleReadWritePermissionsConsistency = ({ + flatRole, +}: { + flatRole: FlatRole; +}): FlatEntityValidationError[] => { + const errors: FlatEntityValidationError[] = []; + + if ( + flatRole.canReadAllObjectRecords === false && + (flatRole.canUpdateAllObjectRecords || + flatRole.canSoftDeleteAllObjectRecords || + flatRole.canDestroyAllObjectRecords) + ) { + errors.push({ + code: PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION, + message: + PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION, + userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`, + }); + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util.ts new file mode 100644 index 0000000000..44ee59d159 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util.ts @@ -0,0 +1,28 @@ +import { msg, t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { FLAT_ROLE_REQUIRED_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants'; +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; + +export const validateRoleRequiredPropertiesAreDefined = ({ + flatRole, +}: { + flatRole: FlatRole; +}): FlatEntityValidationError[] => + FLAT_ROLE_REQUIRED_PROPERTIES.flatMap( + (property) => { + if (isDefined(flatRole[property])) { + return []; + } + + return [ + { + code: PermissionsExceptionCode.INVALID_ARG, + message: t`Property ${property} is required for role`, + userFriendlyMessage: msg`Some of the information provided is invalid. Please check your input and try again.`, + }, + ]; + }, + ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts index 04930f1b95..ec07fcb6c3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts @@ -7,6 +7,8 @@ import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-m import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service'; import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service'; import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service'; +import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service'; +import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service'; import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-route-trigger-validator.service'; import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-serverless-function-validator.service'; import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service'; @@ -29,6 +31,8 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace FlatCronTriggerValidatorService, FlatFieldMetadataTypeValidatorService, FlatRouteTriggerValidatorService, + FlatRoleValidatorService, + FlatRoleTargetValidatorService, ], exports: [ FlatViewValidatorService, @@ -43,6 +47,8 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace FlatCronTriggerValidatorService, FlatFieldMetadataTypeValidatorService, FlatRouteTriggerValidatorService, + FlatRoleValidatorService, + FlatRoleTargetValidatorService, ], }) export class WorkspaceMigrationBuilderValidatorsModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts index a0d0f52b34..4b348901c8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts @@ -7,6 +7,8 @@ import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 's import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service'; import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service'; import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service'; +import { WorkspaceMigrationV2RoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service'; +import { WorkspaceMigrationV2RoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service'; import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service'; import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service'; import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service'; @@ -30,6 +32,8 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationV2FieldActionsBuilderService, WorkspaceMigrationV2CronTriggerActionsBuilderService, WorkspaceMigrationV2RouteTriggerActionsBuilderService, + WorkspaceMigrationV2RoleActionsBuilderService, + WorkspaceMigrationV2RoleTargetActionsBuilderService, ], exports: [ WorkspaceMigrationV2ViewActionsBuilderService, @@ -43,6 +47,8 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService, WorkspaceMigrationV2CronTriggerActionsBuilderService, WorkspaceMigrationV2RouteTriggerActionsBuilderService, + WorkspaceMigrationV2RoleActionsBuilderService, + WorkspaceMigrationV2RoleTargetActionsBuilderService, FlatFieldMetadataTypeValidatorService, ], }) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service.ts new file mode 100644 index 0000000000..11414b2594 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; + +import { + OptimisticallyApplyActionOnAllFlatEntityMapsArgs, + WorkspaceMigrationRunnerActionHandler, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { CreateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create_role_target', +) { + optimisticallyApplyActionOnAllFlatEntityMaps({ + action, + allFlatEntityMaps, + }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { + const { flatRoleTargetMaps } = allFlatEntityMaps; + const { roleTarget } = action; + + const updatedFlatRoleTargetMaps = addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: roleTarget, + flatEntityMaps: flatRoleTargetMaps, + }); + + return { + flatRoleTargetMaps: updatedFlatRoleTargetMaps, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { roleTarget } = action; + + const roleTargetRepository = + queryRunner.manager.getRepository(RoleTargetsEntity); + + await roleTargetRepository.insert({ + ...roleTarget, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service.ts new file mode 100644 index 0000000000..e488cdbcba --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; + +import { + OptimisticallyApplyActionOnAllFlatEntityMapsArgs, + WorkspaceMigrationRunnerActionHandler, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { DeleteRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete_role_target', +) { + constructor() { + super(); + } + + optimisticallyApplyActionOnAllFlatEntityMaps({ + action, + allFlatEntityMaps, + }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { + const { flatRoleTargetMaps } = allFlatEntityMaps; + const { roleTargetId } = action; + + const updatedFlatRoleTargetMaps = deleteFlatEntityFromFlatEntityMapsOrThrow( + { + entityToDeleteId: roleTargetId, + flatEntityMaps: flatRoleTargetMaps, + }, + ); + + return { + flatRoleTargetMaps: updatedFlatRoleTargetMaps, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { roleTargetId } = action; + + const roleTargetRepository = + queryRunner.manager.getRepository(RoleTargetsEntity); + + await roleTargetRepository.delete({ + id: roleTargetId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service.ts new file mode 100644 index 0000000000..212dfe5077 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@nestjs/common'; + +import { + OptimisticallyApplyActionOnAllFlatEntityMapsArgs, + WorkspaceMigrationRunnerActionHandler, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { UpdateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; +import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; + +@Injectable() +export class UpdateRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update_role_target', +) { + constructor() { + super(); + } + + optimisticallyApplyActionOnAllFlatEntityMaps({ + action, + allFlatEntityMaps, + }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { + const { flatRoleTargetMaps } = allFlatEntityMaps; + const { roleTargetId } = action; + + const existingRoleTarget = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: roleTargetId, + flatEntityMaps: flatRoleTargetMaps, + }); + + const updatedRoleTarget = { + ...existingRoleTarget, + ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action), + }; + + const updatedFlatRoleTargetMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: updatedRoleTarget, + flatEntityMaps: flatRoleTargetMaps, + }); + + return { + flatRoleTargetMaps: updatedFlatRoleTargetMaps, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner } = context; + const { roleTargetId } = action; + + const roleTargetRepository = + queryRunner.manager.getRepository(RoleTargetsEntity); + + const update = fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action); + + await roleTargetRepository.update(roleTargetId, update); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service.ts new file mode 100644 index 0000000000..a0d3279352 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; + +import { + OptimisticallyApplyActionOnAllFlatEntityMapsArgs, + WorkspaceMigrationRunnerActionHandler, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { CreateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create_role', +) { + constructor() { + super(); + } + + optimisticallyApplyActionOnAllFlatEntityMaps({ + action, + allFlatEntityMaps, + }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { + const { flatRoleMaps } = allFlatEntityMaps; + const { role } = action; + + const updatedFlatRoleMaps = addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: role, + flatEntityMaps: flatRoleMaps, + }); + + return { + flatRoleMaps: updatedFlatRoleMaps, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { role } = action; + + const roleRepository = + queryRunner.manager.getRepository(RoleEntity); + + await roleRepository.insert({ + ...role, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service.ts new file mode 100644 index 0000000000..ccad0472b2 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; + +import { + OptimisticallyApplyActionOnAllFlatEntityMapsArgs, + WorkspaceMigrationRunnerActionHandler, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { DeleteRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete_role', +) { + constructor() { + super(); + } + + optimisticallyApplyActionOnAllFlatEntityMaps({ + action, + allFlatEntityMaps, + }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { + const { flatRoleMaps } = allFlatEntityMaps; + const { roleId } = action; + + const updatedFlatRoleMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: roleId, + flatEntityMaps: flatRoleMaps, + }); + + return { + flatRoleMaps: updatedFlatRoleMaps, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { roleId } = action; + + const roleRepository = + queryRunner.manager.getRepository(RoleEntity); + + await roleRepository.delete({ id: roleId, workspaceId }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service.ts new file mode 100644 index 0000000000..ed43f0a94a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; + +import { + OptimisticallyApplyActionOnAllFlatEntityMapsArgs, + WorkspaceMigrationRunnerActionHandler, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { UpdateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; +import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; + +@Injectable() +export class UpdateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update_role', +) { + optimisticallyApplyActionOnAllFlatEntityMaps({ + action, + allFlatEntityMaps, + }: OptimisticallyApplyActionOnAllFlatEntityMapsArgs): Partial { + const { flatRoleMaps } = allFlatEntityMaps; + const { roleId } = action; + + const existingRole = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: roleId, + flatEntityMaps: flatRoleMaps, + }); + + const updatedRole = { + ...existingRole, + ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action), + }; + + const updatedFlatRoleMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: updatedRole, + flatEntityMaps: flatRoleMaps, + }); + + return { + flatRoleMaps: updatedFlatRoleMaps, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { roleId } = action; + + const roleRepository = + queryRunner.manager.getRepository(RoleEntity); + + await roleRepository.update( + { id: roleId, workspaceId }, + fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action), + ); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index 673436333d..06cb5363e1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -15,6 +15,12 @@ import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/wo import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service'; import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service'; import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service'; +import { CreateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service'; +import { DeleteRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service'; +import { UpdateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service'; +import { CreateRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service'; +import { DeleteRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service'; +import { UpdateRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service'; import { CreateRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/create-route-trigger-action-handler.service'; import { DeleteRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/delete-route-trigger-action-handler.service'; import { UpdateRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/update-route-trigger-action-handler.service'; @@ -79,6 +85,14 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor CreateRouteTriggerActionHandlerService, DeleteRouteTriggerActionHandlerService, UpdateRouteTriggerActionHandlerService, + + CreateRoleActionHandlerService, + UpdateRoleActionHandlerService, + DeleteRoleActionHandlerService, + + CreateRoleTargetActionHandlerService, + DeleteRoleTargetActionHandlerService, + UpdateRoleTargetActionHandlerService, ], }) export class WorkspaceSchemaMigrationRunnerActionHandlersModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts index b48345c7a5..4fb4621883 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts @@ -60,11 +60,6 @@ export class WorkspaceMigrationRunnerV2Service { workspaceId, ), ); - asyncOperations.push( - this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - }), - ); } const shouldInvalidFindCoreViewsGraphqlCacheOperation = actions.some( @@ -103,6 +98,43 @@ export class WorkspaceMigrationRunnerV2Service { ); } + const shouldInvalidateRoleMapCache = actions.some((action) => { + switch (action.type) { + case 'create_role': + case 'delete_role': + case 'update_role': + case 'create_role_target': + case 'delete_role_target': + case 'update_role_target': { + return true; + } + default: { + return false; + } + } + }); + + if ( + shouldIncrementMetadataGraphqlSchemaVersion || + shouldInvalidateRoleMapCache + ) { + asyncOperations.push( + ...[ + this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({ + workspaceId, + }), + this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ + workspaceId, + }), + this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache( + { + workspaceId, + }, + ), + ], + ); + } + return asyncOperations; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts index e03355b929..b107430b83 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; import { fromStandardRoleDefinitionToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util'; import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface'; @@ -12,8 +13,8 @@ export class StandardRoleFactory { roleDefinitions: StandardRoleDefinition[], context: WorkspaceSyncContext, existingRoles: RoleEntity[], - ): Partial[] { - const computedRoles: Partial[] = []; + ): FlatRole[] { + const computedRoles: FlatRole[] = []; for (const roleDefinition of roleDefinitions) { const existingRole = existingRoles.find( @@ -30,6 +31,7 @@ export class StandardRoleFactory { ...flatRole, id: existingRole.id, universalIdentifier: roleDefinition.standardId || existingRole.id, + createdAt: existingRole.createdAt, }); } else { computedRoles.push({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts index 6b039bf8ec..a9793fa78b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts @@ -38,7 +38,12 @@ export class WorkspaceSyncRoleService { workspaceId: context.workspaceId, standardId: Not(IsNull()), }, - relations: ['permissionFlags'], + relations: [ + 'permissionFlags', + 'roleTargets', + 'objectPermissions', + 'fieldPermissions', + ], }); const targetStandardRoles = this.standardRoleFactory.create( @@ -49,7 +54,7 @@ export class WorkspaceSyncRoleService { const roleComparatorResults = this.workspaceRoleComparator.compare({ fromFlatRoles: existingStandardRoleEntities.map(fromRoleEntityToFlatRole), - toFlatRoles: targetStandardRoles.map(fromRoleEntityToFlatRole), + toFlatRoles: targetStandardRoles, }); for (const roleComparatorResult of roleComparatorResults) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts index 1c1cdb5d54..3465c805f1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts @@ -3,7 +3,16 @@ import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions export type StandardRoleDefinition = Omit< FlatRole, - 'id' | 'workspaceId' | 'universalIdentifier' | 'standardId' + | 'id' + | 'workspaceId' + | 'universalIdentifier' + | 'standardId' + | 'roleTargetIds' + | 'objectPermissionIds' + | 'permissionFlagIds' + | 'fieldPermissionIds' + | 'createdAt' + | 'updatedAt' > & { standardId: string; permissionFlags?: PermissionFlagType[]; diff --git a/packages/twenty-server/test/integration/graphql/suites/role/role.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/role/role.integration-spec.ts deleted file mode 100644 index 3ec18eef44..0000000000 --- a/packages/twenty-server/test/integration/graphql/suites/role/role.integration-spec.ts +++ /dev/null @@ -1,325 +0,0 @@ -import gql from 'graphql-tag'; -import { default as request } from 'supertest'; -import { createRoleOperation } from 'test/integration/graphql/utils/create-custom-role-operation-factory.util'; -import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; - -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; - -const client = request(`http://localhost:${APP_PORT}`); - -describe('Role Permissions Validation', () => { - describe('validateRoleDoesNotHaveWritingPermissionsWithoutReadingPermissionsOrThrow', () => { - describe('createRole - Valid Cases', () => { - it('should allow creating role with read=true and any write permissions', async () => { - const operation = createRoleOperation({ - label: 'ValidRole', - description: 'Valid role with read and write permissions', - canUpdateAllSettings: true, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - }); - - const response = await makeGraphqlAPIRequest(operation); - - expect(response.body.errors).toBeUndefined(); - expect(response.body.data.createOneRole).toBeDefined(); - expect(response.body.data.createOneRole.label).toBe('ValidRole'); - }); - - it('should allow creating role with read=false and all write permissions=false', async () => { - const operation = createRoleOperation({ - label: 'ValidNoWriteRole', - description: 'Valid role with no write permissions', - canUpdateAllSettings: false, - canReadAllObjectRecords: false, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - }); - - const response = await makeGraphqlAPIRequest(operation); - - expect(response.body.errors).toBeUndefined(); - expect(response.body.data.createOneRole).toBeDefined(); - expect(response.body.data.createOneRole.label).toBe('ValidNoWriteRole'); - }); - }); - - describe('createRole - Invalid Cases', () => { - it('should throw error when creating role with read=false but canDestroyAllObjectRecords=true', async () => { - const operation = createRoleOperation({ - label: 'InvalidDestroyRole', - description: 'Invalid role with destroy permission but no read', - canUpdateAllSettings: false, - canReadAllObjectRecords: false, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: true, - }); - - const response = await makeGraphqlAPIRequest(operation); - - expect(response.body.data).toBeNull(); - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toBe( - PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION, - ); - expect(response.body.errors[0].extensions.code).toBe( - ErrorCode.BAD_USER_INPUT, - ); - }); - }); - - describe('updateRole - Valid Cases', () => { - let baseRoleId: string; - - beforeEach(async () => { - const operation = createRoleOperation({ - label: 'BaseRole', - description: 'Base role for update tests', - canUpdateAllSettings: false, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - }); - - const response = await makeGraphqlAPIRequest(operation); - - baseRoleId = response.body.data.createOneRole.id; - }); - - afterEach(async () => { - if (baseRoleId) { - await deleteRole(client, baseRoleId); - } - }); - - it('should allow updating role to have read=true and any write permissions', async () => { - const updateRoleOperation = { - query: gql` - mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { - updateOneRole(updateRoleInput: $updateRoleInput) { - id - canReadAllObjectRecords - canUpdateAllObjectRecords - canSoftDeleteAllObjectRecords - canDestroyAllObjectRecords - } - } - `, - variables: { - updateRoleInput: { - id: baseRoleId, - update: { - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - }, - }, - }, - }; - - const response = await makeGraphqlAPIRequest(updateRoleOperation); - - expect(response.body.errors).toBeUndefined(); - expect(response.body.data.updateOneRole).toBeDefined(); - expect(response.body.data.updateOneRole.canReadAllObjectRecords).toBe( - true, - ); - expect(response.body.data.updateOneRole.canUpdateAllObjectRecords).toBe( - true, - ); - expect( - response.body.data.updateOneRole.canSoftDeleteAllObjectRecords, - ).toBe(true); - expect( - response.body.data.updateOneRole.canDestroyAllObjectRecords, - ).toBe(true); - }); - - it('should allow updating role to have read=false and all write permissions=false', async () => { - const updateRoleOperation = { - query: gql` - mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { - updateOneRole(updateRoleInput: $updateRoleInput) { - id - canReadAllObjectRecords - canUpdateAllObjectRecords - canSoftDeleteAllObjectRecords - canDestroyAllObjectRecords - } - } - `, - variables: { - updateRoleInput: { - id: baseRoleId, - update: { - canReadAllObjectRecords: false, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - }, - }, - }, - }; - - const response = await makeGraphqlAPIRequest(updateRoleOperation); - - expect(response.body.errors).toBeUndefined(); - expect(response.body.data.updateOneRole).toBeDefined(); - expect(response.body.data.updateOneRole.canReadAllObjectRecords).toBe( - false, - ); - expect(response.body.data.updateOneRole.canUpdateAllObjectRecords).toBe( - false, - ); - expect( - response.body.data.updateOneRole.canSoftDeleteAllObjectRecords, - ).toBe(false); - expect( - response.body.data.updateOneRole.canDestroyAllObjectRecords, - ).toBe(false); - }); - }); - - describe('updateRole - Invalid Cases', () => { - let roleWithWritePermissionsId: string; - - beforeEach(async () => { - const operation = createRoleOperation({ - label: 'RoleWithWritePermissions', - description: 'Role with write permissions for update tests', - canUpdateAllSettings: false, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - }); - - const response = await makeGraphqlAPIRequest(operation); - - roleWithWritePermissionsId = response.body.data.createOneRole.id; - }); - - afterEach(async () => { - if (roleWithWritePermissionsId) { - await deleteRole(client, roleWithWritePermissionsId); - } - }); - - it('should throw error when updating role to read=false but keeping write permissions', async () => { - const updateRoleOperation = { - query: gql` - mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { - updateOneRole(updateRoleInput: $updateRoleInput) { - id - } - } - `, - variables: { - updateRoleInput: { - id: roleWithWritePermissionsId, - update: { - canReadAllObjectRecords: false, - // Not explicitly setting write permissions, so they keep existing values (true) - }, - }, - }, - }; - - const response = await makeGraphqlAPIRequest(updateRoleOperation); - - expect(response.body.data).toBeNull(); - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toBe( - PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION, - ); - expect(response.body.errors[0].extensions.code).toBe( - ErrorCode.BAD_USER_INPUT, - ); - }); - - it('should allow updating role to read=false when explicitly setting all write permissions to false', async () => { - const updateRoleOperation = { - query: gql` - mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { - updateOneRole(updateRoleInput: $updateRoleInput) { - id - canReadAllObjectRecords - canUpdateAllObjectRecords - canSoftDeleteAllObjectRecords - canDestroyAllObjectRecords - } - } - `, - variables: { - updateRoleInput: { - id: roleWithWritePermissionsId, - update: { - canReadAllObjectRecords: false, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - }, - }, - }, - }; - - const response = await makeGraphqlAPIRequest(updateRoleOperation); - - expect(response.body.errors).toBeUndefined(); - expect(response.body.data.updateOneRole).toBeDefined(); - expect(response.body.data.updateOneRole.canReadAllObjectRecords).toBe( - false, - ); - expect(response.body.data.updateOneRole.canUpdateAllObjectRecords).toBe( - false, - ); - expect( - response.body.data.updateOneRole.canSoftDeleteAllObjectRecords, - ).toBe(false); - expect( - response.body.data.updateOneRole.canDestroyAllObjectRecords, - ).toBe(false); - }); - - it('should allow updating role to read=false when explicitly setting some write permissions to false', async () => { - const updateRoleOperation = { - query: gql` - mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { - updateOneRole(updateRoleInput: $updateRoleInput) { - id - canReadAllObjectRecords - canUpdateAllObjectRecords - canSoftDeleteAllObjectRecords - canDestroyAllObjectRecords - } - } - `, - variables: { - updateRoleInput: { - id: roleWithWritePermissionsId, - update: { - canSoftDeleteAllObjectRecords: false, - // Keep other permissions as they are - }, - }, - }, - }; - - const response = await makeGraphqlAPIRequest(updateRoleOperation); - - expect(response.body.errors).toBeUndefined(); - expect(response.body.data.updateOneRole).toBeDefined(); - expect( - response.body.data.updateOneRole.canSoftDeleteAllObjectRecords, - ).toBe(false); - }); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts index e8999a8d29..3b29e38288 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts @@ -1,10 +1,13 @@ import request from 'supertest'; import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util'; import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util'; -import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util'; +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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util'; +import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util'; +import { updateWorkspaceMemberRole } from 'test/integration/metadata/suites/role/utils/update-workspace-member-role.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; @@ -20,25 +23,9 @@ describe('Granular settings permissions', () => { beforeAll(async () => { // Get the original Member role ID for restoration later - const getRolesQuery = { - query: ` - query GetRoles { - getRoles { - id - label - } - } - `, - }; + const memberRole = await findOneRoleByLabel({ label: 'Member' }); - const rolesResponse = await client - .post('/graphql') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) - .send(getRolesQuery); - - originalMemberRoleId = rolesResponse.body.data.getRoles.find( - (role: any) => role.label === 'Member', - ).id; + originalMemberRoleId = memberRole.id; // Create a custom role with canUpdateAllSettings = false const createRoleQuery = { @@ -91,31 +78,23 @@ describe('Granular settings permissions', () => { // Assign the custom role to JONY (who uses APPLE_JONY_MEMBER_ACCESS_TOKEN) await updateWorkspaceMemberRole({ - client, - roleId: customRoleId, - workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY, + input: { + roleId: customRoleId, + workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY, + }, + expectToFail: false, }); }); afterAll(async () => { // Restore JONY's original Member role - const restoreMemberRoleQuery = { - query: ` - mutation UpdateWorkspaceMemberRole { - updateWorkspaceMemberRole( - workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}" - roleId: "${originalMemberRoleId}" - ) { - id - } - } - `, - }; - - await client - .post('/graphql') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) - .send(restoreMemberRoleQuery); + await updateWorkspaceMemberRole({ + input: { + workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY, + roleId: originalMemberRoleId, + }, + expectToFail: false, + }); // Delete the custom role const deleteRoleQuery = deleteOneRoleOperationFactory(customRoleId); @@ -346,38 +325,32 @@ describe('Granular settings permissions', () => { describe('Permission Inheritance', () => { it('should verify that canUpdateAllSettings=false is properly overridden by specific setting permissions', async () => { - // Verify the role configuration - const getRoleQuery = { - query: ` - query GetRole { - getRoles { - id - label - canUpdateAllSettings - permissionFlags { - flag - } - } + // Verify the role configuration using the new integration test utilities + const { data, errors } = await findRoles({ + gqlFields: ` + id + label + canUpdateAllSettings + permissionFlags { + flag } `, - }; + expectToFail: false, + }); - const response = await client - .post('/graphql') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) - .send(getRoleQuery); + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); - const customRole = response.body.data.getRoles.find( - (role: any) => role.id === customRoleId, - ); + const customRole = data.getRoles.find((role) => role.id === customRoleId); - expect(customRole).toBeDefined(); + jestExpectToBeDefined(customRole); expect(customRole.canUpdateAllSettings).toBe(false); expect(customRole.permissionFlags).toHaveLength(3); - expect(customRole.permissionFlags.map((p: any) => p.flag)).toContain( + jestExpectToBeDefined(customRole.permissionFlags); + expect(customRole.permissionFlags.map((p) => p.flag)).toContain( PermissionFlagType.DATA_MODEL, ); - expect(customRole.permissionFlags.map((p: any) => p.flag)).toContain( + expect(customRole.permissionFlags.map((p) => p.flag)).toContain( PermissionFlagType.WORKSPACE, ); }); @@ -410,33 +383,28 @@ describe('Granular settings permissions', () => { expect(response.body.errors).toBeUndefined(); expect(response.body.data.upsertPermissionFlags).toHaveLength(3); - // Verify the user now has access to security operations - // Note: This would require a specific security operation to test - // For now, we just verify the permission was added - const getRoleQuery = { - query: ` - query GetRole { - getRoles { - id - permissionFlags { - flag - } - } + // Verify the permission was added using the new integration test utilities + const { data, errors } = await findRoles({ + gqlFields: ` + id + permissionFlags { + flag } `, - }; + expectToFail: false, + }); - const roleResponse = await client - .post('/graphql') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) - .send(getRoleQuery); + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); - const updatedRole = roleResponse.body.data.getRoles.find( - (role: any) => role.id === customRoleId, + const updatedRole = data.getRoles.find( + (role) => role.id === customRoleId, ); + jestExpectToBeDefined(updatedRole); expect(updatedRole.permissionFlags).toHaveLength(3); - expect(updatedRole.permissionFlags.map((p: any) => p.flag)).toContain( + jestExpectToBeDefined(updatedRole.permissionFlags); + expect(updatedRole.permissionFlags.map((p) => p.flag)).toContain( PermissionFlagType.SECURITY, ); }); @@ -467,31 +435,28 @@ describe('Granular settings permissions', () => { expect(response.body.errors).toBeUndefined(); expect(response.body.data.upsertPermissionFlags).toHaveLength(2); - // Verify SECURITY permission was removed - const getRoleQuery = { - query: ` - query GetRole { - getRoles { - id - permissionFlags { - flag - } - } + // Verify SECURITY permission was removed using the new integration test utilities + const { data, errors } = await findRoles({ + gqlFields: ` + id + permissionFlags { + flag } `, - }; + expectToFail: false, + }); - const roleResponse = await client - .post('/graphql') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) - .send(getRoleQuery); + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); - const updatedRole = roleResponse.body.data.getRoles.find( - (role: any) => role.id === customRoleId, + const updatedRole = data.getRoles.find( + (role) => role.id === customRoleId, ); + jestExpectToBeDefined(updatedRole); + jestExpectToBeDefined(updatedRole.permissionFlags); expect(updatedRole.permissionFlags).toHaveLength(2); - expect(updatedRole.permissionFlags.map((p: any) => p.flag)).not.toContain( + expect(updatedRole.permissionFlags.map((p) => p.flag)).not.toContain( PermissionFlagType.SECURITY, ); }); 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 34c78b83f0..a8bc5e7aeb 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 @@ -392,34 +392,6 @@ describe('roles permissions', () => { await assertPermissionDeniedForMemberWithMemberRole({ query }); }); - it('should throw an error when role is not editable', async () => { - const query = { - query: ` - mutation UpdateOneRole { - updateOneRole(updateRoleInput: {id: "${adminRoleId}", update: {label: "new role label (2)"}}) { - id - } - } - `, - }; - - await client - .post('/graphql') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) - .send(query) - .expect(200) - .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, - ); - }); - }); - it('should update a role when user has permission to update a role (admin role)', async () => { const query = { query: ` diff --git a/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts index 2707c61649..45c8af49e5 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts @@ -47,7 +47,7 @@ describe('Destroy core view', () => { }); }); - it.only('should destroy an existing view', async () => { + it('should destroy an existing view', async () => { const { data: { createCoreView: view }, } = await createOneCoreView({ diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap index 8cfbf8c590..2a3ad612d2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap @@ -10,6 +10,8 @@ exports[`View Field Resolver - Failing Create Many Operations - v2 should accumu "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -78,6 +80,8 @@ exports[`View Field Resolver - Failing Create Many Operations - v2 should accumu "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap index cf4c338db6..70b696c15d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap @@ -10,6 +10,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 Should fail to crea "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -41,6 +43,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 Should fail to crea "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -66,6 +70,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -97,6 +103,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -122,6 +130,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -153,6 +163,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap index 52c03dc573..d1c063da78 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap @@ -10,6 +10,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -41,6 +43,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -66,6 +70,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -97,6 +103,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -122,6 +130,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -151,6 +161,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -176,6 +188,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -207,6 +221,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -232,6 +248,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -263,6 +281,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap index ddf83e9535..22e49fb5d2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap @@ -10,6 +10,8 @@ exports[`View Group Resolver - Failing Create Many Operations - v2 should accumu "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -78,6 +80,8 @@ exports[`View Group Resolver - Failing Create Many Operations - v2 should accumu "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/create-one-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/create-one-field-metadata.integration-spec.ts.snap index 644f52d61e..2e10dfa43d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/create-one-field-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/create-one-field-metadata.integration-spec.ts.snap @@ -28,6 +28,8 @@ exports[`createOne FieldMetadataService name/label sync should return an error w ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -42,6 +44,8 @@ exports[`createOne FieldMetadataService name/label sync should return an error w "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap index f3f62455dd..8de33c7628 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap @@ -29,6 +29,8 @@ exports[`Failing create field metadata tests suite should fail to create NUMERIC ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -43,6 +45,8 @@ exports[`Failing create field metadata tests suite should fail to create NUMERIC "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -88,6 +92,8 @@ exports[`Failing create field metadata tests suite should fail to create POSITIO ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -102,6 +108,8 @@ exports[`Failing create field metadata tests suite should fail to create POSITIO "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -140,6 +148,8 @@ exports[`Failing create field metadata tests suite should fail to create TS_VECT "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -154,6 +164,8 @@ exports[`Failing create field metadata tests suite should fail to create TS_VECT "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata.integration-spec.ts.snap index 09c7f7a047..0a7864f32a 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata.integration-spec.ts.snap @@ -28,6 +28,8 @@ exports[`updateOne failing update should not update a field name if it is not sy ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -42,6 +44,8 @@ exports[`updateOne failing update should not update a field name if it is not sy "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -92,6 +96,8 @@ exports[`updateOne failing update should throw if the field name is not availabl ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -106,6 +112,8 @@ exports[`updateOne failing update should throw if the field name is not availabl "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-field-metadata-enum-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-field-metadata-enum-v2.integration-spec.ts.snap index 21044fc4dc..5050922fc6 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-field-metadata-enum-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-field-metadata-enum-v2.integration-spec.ts.snap @@ -29,6 +29,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -43,6 +45,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -88,6 +92,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -102,6 +108,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -146,6 +154,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -160,6 +170,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -219,6 +231,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -233,6 +247,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -292,6 +308,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -306,6 +324,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -365,6 +385,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -379,6 +401,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -438,6 +462,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -452,6 +478,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -496,6 +524,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -510,6 +540,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -555,6 +587,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -569,6 +603,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -618,6 +654,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -632,6 +670,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -676,6 +716,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -690,6 +732,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -735,6 +779,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -749,6 +795,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -793,6 +841,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -807,6 +857,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -866,6 +918,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -880,6 +934,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -924,6 +980,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -938,6 +996,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -982,6 +1042,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -996,6 +1058,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1041,6 +1105,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1055,6 +1121,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1100,6 +1168,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1114,6 +1184,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1158,6 +1230,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1172,6 +1246,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1217,6 +1293,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1231,6 +1309,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1275,6 +1355,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1289,6 +1371,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1338,6 +1422,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1352,6 +1438,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1396,6 +1484,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1410,6 +1500,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1455,6 +1547,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1469,6 +1563,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1513,6 +1609,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1527,6 +1625,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1576,6 +1676,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1590,6 +1692,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1634,6 +1738,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1648,6 +1754,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1693,6 +1801,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1707,6 +1817,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1751,6 +1863,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1765,6 +1879,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1814,6 +1930,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1828,6 +1946,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1872,6 +1992,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1886,6 +2008,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1930,6 +2054,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1944,6 +2070,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1993,6 +2121,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2007,6 +2137,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2051,6 +2183,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2065,6 +2199,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2109,6 +2245,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2123,6 +2261,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2168,6 +2308,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2182,6 +2324,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2226,6 +2370,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2240,6 +2386,8 @@ exports[`Failing create field metadata MULTI_SELECT tests suite v2 Create should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2289,6 +2437,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2303,6 +2453,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2347,6 +2499,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2361,6 +2515,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2405,6 +2561,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2419,6 +2577,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2478,6 +2638,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2492,6 +2654,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2551,6 +2715,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2565,6 +2731,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2624,6 +2792,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2638,6 +2808,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2697,6 +2869,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2711,6 +2885,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2756,6 +2932,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2770,6 +2948,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2819,6 +2999,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2833,6 +3015,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2877,6 +3061,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2891,6 +3077,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2936,6 +3124,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2950,6 +3140,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2994,6 +3186,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3008,6 +3202,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3052,6 +3248,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3066,6 +3264,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3110,6 +3310,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3124,6 +3326,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3169,6 +3373,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3183,6 +3389,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3227,6 +3435,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3241,6 +3451,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3286,6 +3498,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3300,6 +3514,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3344,6 +3560,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3358,6 +3576,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3407,6 +3627,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3421,6 +3643,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3465,6 +3689,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3479,6 +3705,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3524,6 +3752,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3538,6 +3768,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3582,6 +3814,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3596,6 +3830,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3645,6 +3881,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3659,6 +3897,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3703,6 +3943,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3717,6 +3959,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3762,6 +4006,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3776,6 +4022,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3820,6 +4068,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3834,6 +4084,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3883,6 +4135,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3897,6 +4151,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3941,6 +4197,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3955,6 +4213,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3999,6 +4259,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4013,6 +4275,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4062,6 +4326,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4076,6 +4342,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4120,6 +4388,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4134,6 +4404,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4178,6 +4450,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4192,6 +4466,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4237,6 +4513,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4251,6 +4529,8 @@ exports[`Failing create field metadata SELECT tests suite v2 Create should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata-v2.integration-spec.ts.snap index 4339031111..3d0fa9a94c 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata-v2.integration-spec.ts.snap @@ -29,6 +29,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -43,6 +45,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -88,6 +92,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -102,6 +108,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -146,6 +154,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -160,6 +170,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -219,6 +231,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -233,6 +247,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -292,6 +308,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -306,6 +324,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -365,6 +385,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -379,6 +401,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -438,6 +462,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -452,6 +478,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -496,6 +524,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -510,6 +540,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -555,6 +587,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -569,6 +603,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -618,6 +654,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -632,6 +670,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -676,6 +716,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -690,6 +732,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -735,6 +779,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -749,6 +795,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -793,6 +841,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -807,6 +857,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -866,6 +918,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -880,6 +934,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -924,6 +980,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -938,6 +996,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -982,6 +1042,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -996,6 +1058,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1041,6 +1105,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1055,6 +1121,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1100,6 +1168,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1114,6 +1184,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1158,6 +1230,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1172,6 +1246,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1217,6 +1293,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1231,6 +1309,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1275,6 +1355,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1289,6 +1371,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1338,6 +1422,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1352,6 +1438,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1396,6 +1484,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1410,6 +1500,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1454,6 +1546,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1468,6 +1562,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1517,6 +1613,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1531,6 +1629,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1575,6 +1675,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1589,6 +1691,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1634,6 +1738,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1648,6 +1754,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1692,6 +1800,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1706,6 +1816,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1755,6 +1867,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1769,6 +1883,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1813,6 +1929,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1827,6 +1945,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1871,6 +1991,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1885,6 +2007,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1934,6 +2058,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1948,6 +2074,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1992,6 +2120,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2006,6 +2136,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2050,6 +2182,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2064,6 +2198,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2108,6 +2244,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2122,6 +2260,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2166,6 +2306,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2180,6 +2322,8 @@ exports[`Failing update field metadata MULTI_SELECT tests suite v2 Update should "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2229,6 +2373,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2243,6 +2389,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2287,6 +2435,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2301,6 +2451,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2345,6 +2497,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2359,6 +2513,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2418,6 +2574,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2432,6 +2590,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2491,6 +2651,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2505,6 +2667,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2564,6 +2728,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2578,6 +2744,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2637,6 +2805,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2651,6 +2821,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2696,6 +2868,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2710,6 +2884,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2759,6 +2935,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2773,6 +2951,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2817,6 +2997,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2831,6 +3013,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2876,6 +3060,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2890,6 +3076,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2934,6 +3122,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -2948,6 +3138,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2992,6 +3184,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3006,6 +3200,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3050,6 +3246,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3064,6 +3262,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3109,6 +3309,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3123,6 +3325,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3167,6 +3371,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3181,6 +3387,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3226,6 +3434,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3240,6 +3450,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3284,6 +3496,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3298,6 +3512,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3347,6 +3563,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3361,6 +3579,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3405,6 +3625,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3419,6 +3641,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3463,6 +3687,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3477,6 +3703,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3526,6 +3754,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3540,6 +3770,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3584,6 +3816,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3598,6 +3832,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3643,6 +3879,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3657,6 +3895,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3701,6 +3941,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3715,6 +3957,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3764,6 +4008,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3778,6 +4024,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3822,6 +4070,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3836,6 +4086,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3880,6 +4132,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3894,6 +4148,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3943,6 +4199,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -3957,6 +4215,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4001,6 +4261,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4015,6 +4277,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4059,6 +4323,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4073,6 +4339,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4117,6 +4385,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -4131,6 +4401,8 @@ exports[`Failing update field metadata SELECT tests suite v2 Update should fail "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-add-one-target-to-metadata-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-add-one-target-to-metadata-morph-relation-v2.integration-spec.ts.snap index b27af52cb8..9b5972e1ad 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-add-one-target-to-metadata-morph-relation-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-add-one-target-to-metadata-morph-relation-v2.integration-spec.ts.snap @@ -71,6 +71,8 @@ exports[`updateOne FieldMetadataService morph relation fields v2 - Add one targe }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -85,6 +87,8 @@ exports[`updateOne FieldMetadataService morph relation fields v2 - Add one targe "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -159,6 +163,8 @@ exports[`updateOne FieldMetadataService morph relation fields v2 - Add one targe }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -173,6 +179,8 @@ exports[`updateOne FieldMetadataService morph relation fields v2 - Add one targe "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap index 5a289f2ff8..824125e3e8 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap @@ -76,6 +76,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -90,6 +92,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -133,6 +137,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -147,6 +153,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -184,6 +192,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -198,6 +208,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -235,6 +247,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -249,6 +263,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -314,6 +330,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -328,6 +346,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -387,6 +407,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -401,6 +423,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -460,6 +484,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -474,6 +500,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -539,6 +567,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -553,6 +583,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -598,6 +630,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -612,6 +646,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -657,6 +693,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -671,6 +709,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -715,6 +755,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -729,6 +771,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -766,6 +810,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -780,6 +826,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -817,6 +865,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -831,6 +881,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -868,6 +920,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -882,6 +936,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -922,6 +978,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -936,6 +994,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -974,6 +1034,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -988,6 +1050,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1025,6 +1089,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1039,6 +1105,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap index f710b54d43..0a91ee3825 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap @@ -28,6 +28,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -42,6 +44,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -85,6 +89,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -99,6 +105,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -148,6 +156,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -162,6 +172,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -205,6 +217,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -219,6 +233,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -268,6 +284,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -282,6 +300,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -325,6 +345,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -339,6 +361,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -382,6 +406,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -396,6 +422,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -438,6 +466,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -452,6 +482,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -527,6 +559,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE when }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -541,6 +575,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE when "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -584,6 +620,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE when "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -598,6 +636,8 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE when "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -658,6 +698,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -672,6 +714,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -733,6 +777,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -747,6 +793,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -814,6 +862,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -828,6 +878,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -889,6 +941,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -903,6 +957,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -970,6 +1026,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -984,6 +1042,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1029,6 +1089,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1043,6 +1105,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1088,6 +1152,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1102,6 +1168,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1146,6 +1214,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1160,6 +1230,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1237,6 +1309,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY when }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1251,6 +1325,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY when "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1296,6 +1372,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY when "type": "create_field", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1310,6 +1388,8 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY when "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1354,6 +1434,8 @@ exports[`Field metadata relation creation should fail should fail when creating ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1368,6 +1450,8 @@ exports[`Field metadata relation creation should fail should fail when creating "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1427,6 +1511,8 @@ exports[`Field metadata relation creation should fail should fail when creating }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1441,6 +1527,8 @@ exports[`Field metadata relation creation should fail should fail when creating "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1516,6 +1604,8 @@ exports[`Field metadata relation creation should fail should fail when creating }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1530,6 +1620,8 @@ exports[`Field metadata relation creation should fail should fail when creating "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1605,6 +1697,8 @@ exports[`Field metadata relation creation should fail should fail when creating }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -1619,6 +1713,8 @@ exports[`Field metadata relation creation should fail should fail when creating "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap index 322ccc98a6..2578e1a661 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap @@ -35,6 +35,8 @@ exports[`Field metadata relation update should fail relation when name is not in ], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -49,6 +51,8 @@ exports[`Field metadata relation update should fail relation when name is not in "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap index f877f706e2..8e228738c1 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/__snapshots__/create-one-unique-field-metadata.integration-spec.ts.snap @@ -26,6 +26,8 @@ exports[`failing createOne unique field metadata should fail for composite type }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -40,6 +42,8 @@ exports[`failing createOne unique field metadata should fail for composite type "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -81,6 +85,8 @@ exports[`failing createOne unique field metadata should fail when field has cust }, ], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -95,6 +101,8 @@ exports[`failing createOne unique field metadata should fail when field has cust "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap index d28e537acb..907cd5b4c0 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -337,6 +337,8 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -598,6 +600,8 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -950,6 +954,8 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -1211,6 +1217,8 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -1574,6 +1582,8 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -1835,6 +1845,8 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2187,6 +2199,8 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -2448,6 +2462,8 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -2812,6 +2828,8 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] = "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -3073,6 +3091,8 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] = "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -3426,6 +3446,8 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -3687,6 +3709,8 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4069,6 +4093,8 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -4330,6 +4356,8 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -4682,6 +4710,8 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -4943,6 +4973,8 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -5295,6 +5327,8 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -5556,6 +5590,8 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -5924,6 +5960,8 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -6185,6 +6223,8 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -6602,6 +6642,8 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -6863,6 +6905,8 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -7280,6 +7324,8 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -7541,6 +7587,8 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -7923,6 +7971,8 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -8184,6 +8234,8 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -8566,6 +8618,8 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -8827,6 +8881,8 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -9255,6 +9311,8 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -9516,6 +9574,8 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -9869,6 +9929,8 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] = "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -10130,6 +10192,8 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] = "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -10483,6 +10547,8 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res "type": "create_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [ @@ -10744,6 +10810,8 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index c199b171d4..8909ae7507 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -28,6 +28,8 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o "type": "update_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -59,6 +61,8 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -103,6 +107,8 @@ exports[`Object metadata update should fail when labelIdentifier is not a known "type": "update_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -139,6 +145,8 @@ exports[`Object metadata update should fail when labelIdentifier is not a known "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, @@ -200,6 +208,8 @@ exports[`Object metadata update should fail when labelIdentifier is null 1`] = ` "type": "update_object", }, ], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -214,6 +224,8 @@ exports[`Object metadata update should fail when labelIdentifier is null 1`] = ` "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-delete-one-role.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-delete-one-role.integration-spec.ts.snap new file mode 100644 index 0000000000..b3ce4b9d3f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-delete-one-role.integration-spec.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Role deletion should fail when deleting a non-editable system role (Admin) 1`] = ` +[ + { + "extensions": { + "code": "FORBIDDEN", + "subCode": "CANNOT_UNASSIGN_LAST_ADMIN", + "userFriendlyMessage": "You cannot remove the admin role from the last administrator. Please assign another administrator first.", + }, + "message": "Cannot unassign admin role from last admin of the workspace", + "name": "ForbiddenError", + }, +] +`; + +exports[`Role deletion should fail when deleting a non-existent role 1`] = ` +[ + { + "extensions": { + "code": "NOT_FOUND", + "subCode": "ROLE_NOT_FOUND", + "userFriendlyMessage": "The role you are looking for could not be found. It may have been deleted or you may not have access to it.", + }, + "message": "Role not found", + "name": "NotFoundError", + }, +] +`; + +exports[`Role deletion should fail when deleting the workspace default role (Member) 1`] = ` +[ + { + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": Any, + "userFriendlyMessage": "The default role cannot be deleted as it is required for the workspace to function properly.", + }, + "message": "Default role cannot be deleted", + }, +] +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-creation.integration-spec.ts.snap new file mode 100644 index 0000000000..a7b83cf948 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-creation.integration-spec.ts.snap @@ -0,0 +1,310 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Role creation should fail when canReadAllObjectRecords is false but canDestroyAllObjectRecords is true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "label": "Inconsistent Destroy Role", + }, + "status": "fail", + "type": "create_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating role", + "name": "GraphQLError", +} +`; + +exports[`Role creation should fail when canReadAllObjectRecords is false but canSoftDeleteAllObjectRecords is true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "label": "Inconsistent Soft Delete Role", + }, + "status": "fail", + "type": "create_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating role", + "name": "GraphQLError", +} +`; + +exports[`Role creation should fail when canReadAllObjectRecords is false but canUpdateAllObjectRecords is true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "label": "Inconsistent Update Role", + }, + "status": "fail", + "type": "create_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating role", + "name": "GraphQLError", +} +`; + +exports[`Role creation should fail when canReadAllObjectRecords is false but multiple write permissions are true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "label": "Inconsistent Multiple Write Role", + }, + "status": "fail", + "type": "create_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating role", + "name": "GraphQLError", +} +`; + +exports[`Role creation should fail when label already exists 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "ROLE_LABEL_ALREADY_EXISTS", + "message": "A role with this label already exists", + "userFriendlyMessage": "A role with this label already exists.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "label": "Existing Test Role", + }, + "status": "fail", + "type": "create_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating role", + "name": "GraphQLError", +} +`; + +exports[`Role creation should fail when label is missing 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + }, + "message": "Field "label" of required type "String!" was not provided.", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-update.integration-spec.ts.snap new file mode 100644 index 0000000000..d6fbf00976 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/__snapshots__/failing-role-update.integration-spec.ts.snap @@ -0,0 +1,419 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Role update should fail should fail when updating a non-existent role 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "ROLE_NOT_FOUND", + "userFriendlyMessage": "The role you are looking for could not be found. It may have been deleted or you may not have access to it.", + }, + "message": "Role not found", + "name": "NotFoundError", +} +`; + +exports[`Role update should fail updating role with existing write permissions should fail when updating only canReadAllObjectRecords to false while role has existing write permissions 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; + +exports[`Role update should fail when updating a non-editable system role 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "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": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; + +exports[`Role update should fail when updating canReadAllObjectRecords to false while canDestroyAllObjectRecords is true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; + +exports[`Role update should fail when updating canReadAllObjectRecords to false while canSoftDeleteAllObjectRecords is true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; + +exports[`Role update should fail when updating canReadAllObjectRecords to false while canUpdateAllObjectRecords is true 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; + +exports[`Role update should fail when updating label to one that already exists 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "ROLE_LABEL_ALREADY_EXISTS", + "message": "A role with this label already exists", + "userFriendlyMessage": "A role with this label already exists.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; + +exports[`Role update should fail when updating to enable write permissions without read permission 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "role": [ + { + "errors": [ + { + "code": "CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION", + "message": "Cannot give writing permission without reading permission", + "userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + }, + "status": "fail", + "type": "update_role", + }, + ], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while updating role", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/failing-delete-one-role.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/failing-delete-one-role.integration-spec.ts new file mode 100644 index 0000000000..4dafac9019 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/failing-delete-one-role.integration-spec.ts @@ -0,0 +1,91 @@ +import { faker } from '@faker-js/faker'; +import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util'; +import { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +type TestContext = { + input: (testSetup: TestSetup) => { + idToDelete: string; + }; +}; + +type TestSetup = { + adminRoleId: string; + memberRoleId: string; +}; + +type GlobalTestContext = { + adminRoleId: string; + memberRoleId: string; +}; + +const globalTestContext: GlobalTestContext = { + adminRoleId: '', + memberRoleId: '', +}; + +type DeleteOneRoleTestingContext = EachTestingContext[]; + +describe('Role deletion should fail', () => { + beforeAll(async () => { + const adminRole = await findOneRoleByLabel({ label: 'Admin' }); + const memberRole = await findOneRoleByLabel({ label: 'Member' }); + + globalTestContext.adminRoleId = adminRole.id; + globalTestContext.memberRoleId = memberRole.id; + }); + + const failingRoleDeletionTestCases: DeleteOneRoleTestingContext = [ + { + title: 'when deleting a non-editable system role (Admin)', + context: { + input: (testSetup) => ({ + idToDelete: testSetup.adminRoleId, + }), + }, + }, + { + title: 'when deleting the workspace default role (Member)', + context: { + input: (testSetup) => ({ + idToDelete: testSetup.memberRoleId, + }), + }, + }, + { + title: 'when deleting a non-existent role', + context: { + input: () => ({ + idToDelete: faker.string.uuid(), + }), + }, + }, + ]; + + it.each(eachTestingContextFilter(failingRoleDeletionTestCases))( + '$title', + async ({ context }) => { + const testSetup: TestSetup = { + adminRoleId: globalTestContext.adminRoleId, + memberRoleId: globalTestContext.memberRoleId, + }; + + const { idToDelete } = context.input(testSetup); + + const { errors } = await deleteOneRole({ + expectToFail: true, + input: { + idToDelete, + }, + }); + + expect(errors).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(errors), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/failing-role-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/failing-role-creation.integration-spec.ts new file mode 100644 index 0000000000..fa08e112a2 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/failing-role-creation.integration-spec.ts @@ -0,0 +1,161 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.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 { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { isDefined } from 'twenty-shared/utils'; + +import { type CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; + +type TestContext = { + input: CreateRoleInput; +}; + +type GlobalTestContext = { + existingRoleLabel: string; +}; + +const globalTestContext: GlobalTestContext = { + existingRoleLabel: 'Existing Test Role', +}; + +type CreateOneRoleTestingContext = EachTestingContext[]; + +describe('Role creation should fail', () => { + let existingRoleId: string | undefined; + + beforeAll(async () => { + const { data } = await createOneRole({ + expectToFail: false, + input: { + label: globalTestContext.existingRoleLabel, + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }); + + existingRoleId = data.createOneRole.id; + }); + + afterAll(async () => { + if (isDefined(existingRoleId)) { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: existingRoleId }, + }); + } + }); + + const failingRoleCreationTestCases: CreateOneRoleTestingContext = [ + // Missing required properties tests + { + title: 'when label is missing', + context: { + input: { + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + } as CreateRoleInput, + }, + }, + // Label uniqueness test + { + title: 'when label already exists', + context: { + input: { + label: globalTestContext.existingRoleLabel, + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }, + }, + // Read/Write permissions consistency tests + { + title: + 'when canReadAllObjectRecords is false but canUpdateAllObjectRecords is true', + context: { + input: { + label: 'Inconsistent Update Role', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }, + }, + { + title: + 'when canReadAllObjectRecords is false but canSoftDeleteAllObjectRecords is true', + context: { + input: { + label: 'Inconsistent Soft Delete Role', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: false, + }, + }, + }, + { + title: + 'when canReadAllObjectRecords is false but canDestroyAllObjectRecords is true', + context: { + input: { + label: 'Inconsistent Destroy Role', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: true, + }, + }, + }, + { + title: + 'when canReadAllObjectRecords is false but multiple write permissions are true', + context: { + input: { + label: 'Inconsistent Multiple Write Role', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + }, + }, + }, + ]; + + it.each(eachTestingContextFilter(failingRoleCreationTestCases))( + '$title', + async ({ context }) => { + const { errors } = await createOneRole({ + expectToFail: true, + input: context.input, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/failing-role-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/failing-role-update.integration-spec.ts new file mode 100644 index 0000000000..bda4539f36 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/failing-role-update.integration-spec.ts @@ -0,0 +1,271 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.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 { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util'; +import { updateOneRole } from 'test/integration/metadata/suites/role/utils/update-one-role.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type UpdateRolePayload } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; + +type TestContext = { + input: (testSetup: TestSetup) => { + idToUpdate: string; + updatePayload: UpdateRolePayload; + }; +}; + +type TestSetup = { + testRoleId: string; + existingRoleLabelForDuplicate: string; + nonEditableRoleId: string; +}; + +type GlobalTestContext = { + existingRoleLabelForDuplicate: string; + nonEditableRoleId: string; +}; + +const globalTestContext: GlobalTestContext = { + existingRoleLabelForDuplicate: 'Existing Role For Duplicate Test', + nonEditableRoleId: '', +}; + +type UpdateOneRoleTestingContext = EachTestingContext[]; + +describe('Role update should fail', () => { + let testRoleId: string; + let existingRoleIdForDuplicate: string; + + beforeAll(async () => { + // Get a non-editable system role (Admin) for testing + const adminRole = await findOneRoleByLabel({ label: 'Admin' }); + + globalTestContext.nonEditableRoleId = adminRole.id; + + // Create a role that will be used to test duplicate label validation + const { data: duplicateData } = await createOneRole({ + expectToFail: false, + input: { + label: globalTestContext.existingRoleLabelForDuplicate, + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }); + + existingRoleIdForDuplicate = duplicateData.createOneRole.id; + }); + + beforeEach(async () => { + // Create a role for each test + const { data } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Role To Update', + description: 'Original description', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + testRoleId = data.createOneRole.id; + }); + + afterEach(async () => { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: testRoleId }, + }); + }); + + afterAll(async () => { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: existingRoleIdForDuplicate }, + }); + }); + + describe('updating role with existing write permissions', () => { + let roleWithWritePermissionsId: string; + + beforeEach(async () => { + // Create a role with read=true and write=true + const { data } = await createOneRole({ + expectToFail: false, + input: { + label: 'Role With Write Permissions', + description: 'Role with write permissions for update tests', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + }, + }); + + roleWithWritePermissionsId = data.createOneRole.id; + }); + + afterEach(async () => { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: roleWithWritePermissionsId }, + }); + }); + + it('should fail when updating only canReadAllObjectRecords to false while role has existing write permissions', async () => { + const { errors } = await updateOneRole({ + expectToFail: true, + input: { + idToUpdate: roleWithWritePermissionsId, + updatePayload: { + canReadAllObjectRecords: false, + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }); + }); + + const failingRoleUpdateTestCases: UpdateOneRoleTestingContext = [ + { + title: 'when updating label to one that already exists', + context: { + input: (testSetup) => ({ + idToUpdate: testSetup.testRoleId, + updatePayload: { + label: testSetup.existingRoleLabelForDuplicate, + }, + }), + }, + }, + { + title: 'when updating a non-editable system role', + context: { + input: (testSetup) => ({ + idToUpdate: testSetup.nonEditableRoleId, + updatePayload: { + label: 'new role label', + }, + }), + }, + }, + // Read/Write permissions consistency tests + { + title: + 'when updating canReadAllObjectRecords to false while canUpdateAllObjectRecords is true', + context: { + input: (testSetup) => ({ + idToUpdate: testSetup.testRoleId, + updatePayload: { + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: true, + }, + }), + }, + }, + { + title: + 'when updating canReadAllObjectRecords to false while canSoftDeleteAllObjectRecords is true', + context: { + input: (testSetup) => ({ + idToUpdate: testSetup.testRoleId, + updatePayload: { + canReadAllObjectRecords: false, + canSoftDeleteAllObjectRecords: true, + }, + }), + }, + }, + { + title: + 'when updating canReadAllObjectRecords to false while canDestroyAllObjectRecords is true', + context: { + input: (testSetup) => ({ + idToUpdate: testSetup.testRoleId, + updatePayload: { + canReadAllObjectRecords: false, + canDestroyAllObjectRecords: true, + }, + }), + }, + }, + { + title: + 'when updating to enable write permissions without read permission', + context: { + input: (testSetup) => ({ + idToUpdate: testSetup.testRoleId, + updatePayload: { + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + }, + }), + }, + }, + ]; + + it.each(eachTestingContextFilter(failingRoleUpdateTestCases))( + '$title', + async ({ context }) => { + const testSetup: TestSetup = { + testRoleId, + existingRoleLabelForDuplicate: + globalTestContext.existingRoleLabelForDuplicate, + nonEditableRoleId: globalTestContext.nonEditableRoleId, + }; + + const { idToUpdate, updatePayload } = context.input(testSetup); + + const { errors } = await updateOneRole({ + expectToFail: true, + input: { + idToUpdate, + updatePayload, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); + + it('should fail when updating a non-existent role', async () => { + const nonExistentRoleId = faker.string.uuid(); + + const { errors } = await updateOneRole({ + expectToFail: true, + input: { + idToUpdate: nonExistentRoleId, + updatePayload: { + label: 'Updated Label', + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/successful-delete-one-role.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/successful-delete-one-role.integration-spec.ts new file mode 100644 index 0000000000..3390d5dec1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/successful-delete-one-role.integration-spec.ts @@ -0,0 +1,81 @@ +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 { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util'; + +describe('Role deletion should succeed', () => { + it('should successfully delete a custom editable role', async () => { + // Create a custom role that CAN be deleted + const { data: createData, errors: createErrors } = await createOneRole({ + expectToFail: false, + input: { + label: 'Deletable Custom Role', + description: 'A custom role that can be deleted', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }); + + expect(createErrors).toBeUndefined(); + expect(createData.createOneRole.id).toBeDefined(); + + const customRoleId = createData.createOneRole.id; + + // Delete the custom role + const { data, errors } = await deleteOneRole({ + expectToFail: false, + input: { + idToDelete: customRoleId, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.deleteOneRole).toBe(customRoleId); + }); + + it('should delete a custom role and verify it can no longer be found', async () => { + const testLabel = 'Role To Be Deleted And Verified'; + + // Create the role + const { data: createData } = await createOneRole({ + expectToFail: false, + input: { + label: testLabel, + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }); + + const roleId = createData.createOneRole.id; + + // Verify role exists before deletion + const roleBefore = await findOneRoleByLabel({ label: testLabel }); + + expect(roleBefore).toBeDefined(); + expect(roleBefore.id).toBe(roleId); + + // Delete the role + const { data, errors } = await deleteOneRole({ + expectToFail: false, + input: { + idToDelete: roleId, + }, + }); + + expect(errors).toBeUndefined(); + expect(data.deleteOneRole).toBe(roleId); + + // Verify role no longer exists after deletion + await expect(findOneRoleByLabel({ label: testLabel })).rejects.toThrow( + `Role with label "${testLabel}" not found`, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/successful-role-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/successful-role-creation.integration-spec.ts new file mode 100644 index 0000000000..c95d46ff65 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/successful-role-creation.integration-spec.ts @@ -0,0 +1,112 @@ +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'; + +describe('Role creation should succeed', () => { + let createdRoleId: string; + + afterEach(async () => { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: createdRoleId }, + }); + }); + + it('should create a basic custom role with minimal input', async () => { + const { data, errors } = await createOneRole({ + expectToFail: false, + input: { + label: 'Test Custom Role', + description: 'A custom role for integration testing', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + createdRoleId = data?.createOneRole?.id; + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.createOneRole).toMatchObject({ + id: expect.any(String), + label: 'Test Custom Role', + description: 'A custom role for integration testing', + icon: 'IconSettings', + isEditable: true, + canUpdateAllSettings: false, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }); + }); + + it('should create role with read and write permissions', async () => { + const { data, errors } = await createOneRole({ + expectToFail: false, + input: { + label: 'Role With All Permissions', + description: 'Role with read and write permissions', + canUpdateAllSettings: true, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + }, + }); + + createdRoleId = data?.createOneRole?.id; + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.createOneRole).toMatchObject({ + id: expect.any(String), + label: 'Role With All Permissions', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + }); + }); + + it('should create role with read=false and all write permissions=false', async () => { + const { data, errors } = await createOneRole({ + expectToFail: false, + input: { + label: 'Read-Only False Role', + description: 'Role with no read or write permissions', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }); + + createdRoleId = data?.createOneRole?.id; + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.createOneRole).toMatchObject({ + id: expect.any(String), + label: 'Read-Only False Role', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/successful-role-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/successful-role-update.integration-spec.ts new file mode 100644 index 0000000000..17e67c7fda --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/successful-role-update.integration-spec.ts @@ -0,0 +1,261 @@ +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 { updateOneRole } from 'test/integration/metadata/suites/role/utils/update-one-role.util'; + +describe('Role update should succeed', () => { + let testRoleId: string; + + beforeEach(async () => { + const { data } = await createOneRole({ + expectToFail: false, + input: { + label: 'Original Role Label', + description: 'Original description', + icon: 'IconSettings', + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: true, + canBeAssignedToAgents: false, + canBeAssignedToApiKeys: false, + }, + }); + + testRoleId = data.createOneRole.id; + }); + + afterEach(async () => { + await deleteOneRole({ + expectToFail: false, + input: { idToDelete: testRoleId }, + }); + }); + + it('should update role label', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + label: 'Updated Role Label', + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + label: 'Updated Role Label', + description: 'Original description', + icon: 'IconSettings', + }); + }); + + it('should update role description', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + description: 'Updated description for the role', + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + label: 'Original Role Label', + description: 'Updated description for the role', + }); + }); + + it('should update role icon', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + icon: 'IconUser', + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + icon: 'IconUser', + }); + }); + + it('should update role permissions', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + canUpdateAllSettings: true, + canAccessAllTools: true, + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + canUpdateAllSettings: true, + canAccessAllTools: true, + canReadAllObjectRecords: true, + }); + }); + + it('should update role object record permissions', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: false, + }); + }); + + it('should update role assignment permissions', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + canBeAssignedToAgents: true, + canBeAssignedToApiKeys: true, + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + canBeAssignedToUsers: true, + canBeAssignedToAgents: true, + canBeAssignedToApiKeys: true, + }); + }); + + it('should update multiple role properties at once', async () => { + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + label: 'Comprehensive Update', + description: 'Updated multiple fields', + icon: 'IconBriefcase', + canUpdateAllSettings: true, + canAccessAllTools: true, + canUpdateAllObjectRecords: true, + canBeAssignedToAgents: true, + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + label: 'Comprehensive Update', + description: 'Updated multiple fields', + icon: 'IconBriefcase', + canUpdateAllSettings: true, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canBeAssignedToUsers: true, + canBeAssignedToAgents: true, + }); + }); + + it('should update role to read=false when explicitly setting all write permissions to false', async () => { + // First update the role to have write permissions + await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + }, + }, + }); + + // Now update to read=false with all write permissions explicitly set to false + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }); + }); + + it('should update role from read=true to read=false with all write=false', async () => { + // Start with read=true, write=false (default from beforeEach) + const { data, errors } = await updateOneRole({ + expectToFail: false, + input: { + idToUpdate: testRoleId, + updatePayload: { + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(data).toBeDefined(); + expect(data.updateOneRole).toMatchObject({ + id: testRoleId, + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/successful-update-workspace-member-role.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/role/successful-update-workspace-member-role.integration-spec.ts new file mode 100644 index 0000000000..ce57814d1f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/successful-update-workspace-member-role.integration-spec.ts @@ -0,0 +1,110 @@ +import { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util'; +import { updateWorkspaceMemberRole } from 'test/integration/metadata/suites/role/utils/update-workspace-member-role.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; + +import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; + +describe('Workspace member role update should succeed', () => { + let adminRoleId: string; + let guestRoleId: string; + let memberRoleId: string; + let shouldRestorePhilRole: boolean = false; + + beforeAll(async () => { + // Get the Admin, Guest, and Member role IDs + const adminRole = await findOneRoleByLabel({ label: 'Admin' }); + const guestRole = await findOneRoleByLabel({ label: 'Guest' }); + const memberRole = await findOneRoleByLabel({ label: 'Member' }); + + adminRoleId = adminRole.id; + guestRoleId = guestRole.id; + memberRoleId = memberRole.id; + }); + + afterEach(async () => { + if (shouldRestorePhilRole) { + await updateWorkspaceMemberRole({ + input: { + workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, + roleId: guestRoleId, + }, + }); + } + + shouldRestorePhilRole = false; + }); + + it('should update Phil from Guest to Admin role', async () => { + const { data: updateAdminData } = await updateWorkspaceMemberRole({ + expectToFail: false, + input: { + workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, + roleId: adminRoleId, + }, + gqlFields: ` + id + name { + firstName + lastName + } + userEmail + roles { + id + label + icon + } + `, + }); + + shouldRestorePhilRole = true; + + expect(updateAdminData.updateWorkspaceMemberRole).toMatchObject({ + id: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, + name: { + firstName: 'Phil', + lastName: 'Schiler', + }, + }); + jestExpectToBeDefined(updateAdminData.updateWorkspaceMemberRole.roles); + expect(updateAdminData.updateWorkspaceMemberRole.roles).toHaveLength(1); + expect(updateAdminData.updateWorkspaceMemberRole.roles[0]).toMatchObject({ + id: adminRoleId, + label: 'Admin', + }); + + const { data } = await updateWorkspaceMemberRole({ + expectToFail: false, + input: { + workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, + roleId: memberRoleId, + }, + gqlFields: ` + id + name { + firstName + lastName + } + userEmail + roles { + id + label + icon + } + `, + }); + + expect(data.updateWorkspaceMemberRole).toMatchObject({ + id: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL, + name: { + firstName: 'Phil', + lastName: 'Schiler', + }, + }); + jestExpectToBeDefined(data.updateWorkspaceMemberRole.roles); + expect(data.updateWorkspaceMemberRole.roles).toHaveLength(1); + expect(data.updateWorkspaceMemberRole.roles[0]).toMatchObject({ + id: memberRoleId, + label: 'Member', + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role-query-factory.util.ts new file mode 100644 index 0000000000..9f94b9cb88 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role-query-factory.util.ts @@ -0,0 +1,39 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; + +export type CreateOneRoleFactoryInput = CreateRoleInput; + +const DEFAULT_ROLE_GQL_FIELDS = ` + id + label + description + icon + isEditable + canUpdateAllSettings + canAccessAllTools + canReadAllObjectRecords + canUpdateAllObjectRecords + canSoftDeleteAllObjectRecords + canDestroyAllObjectRecords + canBeAssignedToUsers + canBeAssignedToAgents + canBeAssignedToApiKeys +`; + +export const createOneRoleQueryFactory = ({ + input, + gqlFields = DEFAULT_ROLE_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateOneRole($createRoleInput: CreateRoleInput!) { + createOneRole(createRoleInput: $createRoleInput) { + ${gqlFields} + } + } + `, + variables: { + createRoleInput: input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role.util.ts new file mode 100644 index 0000000000..545dd9ed67 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/create-one-role.util.ts @@ -0,0 +1,43 @@ +import { + type CreateOneRoleFactoryInput, + createOneRoleQueryFactory, +} from 'test/integration/metadata/suites/role/utils/create-one-role-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 RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; + +export const createOneRole = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createOneRole: RoleDTO; +}> => { + const graphqlOperation = createOneRoleQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Role creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Role creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role-query-factory.util.ts new file mode 100644 index 0000000000..74b6ce97e1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role-query-factory.util.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DeleteOneRoleFactoryInput = { + idToDelete: string; +}; + +export const deleteOneRoleQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeleteOneRole($idToDelete: UUID!) { + deleteOneRole(roleId: $idToDelete) + } + `, + variables: { + idToDelete: input.idToDelete, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role.util.ts new file mode 100644 index 0000000000..eb825a9830 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/delete-one-role.util.ts @@ -0,0 +1,41 @@ +import { + type DeleteOneRoleFactoryInput, + deleteOneRoleQueryFactory, +} from 'test/integration/metadata/suites/role/utils/delete-one-role-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'; + +export const deleteOneRole = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deleteOneRole: string; +}> => { + const graphqlOperation = deleteOneRoleQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Role deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Role deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/find-one-role-by-label.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-one-role-by-label.util.ts new file mode 100644 index 0000000000..8bcba7c29b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-one-role-by-label.util.ts @@ -0,0 +1,33 @@ +import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util'; + +import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; + +export const findOneRoleByLabel = async ({ + label, + gqlFields, + token, +}: { + label: string; + gqlFields?: string; + token?: string; +}): Promise => { + const { data, errors } = await findRoles({ + gqlFields, + expectToFail: false, + token, + }); + + if (errors || !data?.getRoles) { + throw new Error( + `Failed to get roles: ${JSON.stringify(errors || 'No data returned')}`, + ); + } + + const role = data.getRoles.find((r) => r.label === label); + + if (!role) { + throw new Error(`Role with label "${label}" not found`); + } + + return role; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts new file mode 100644 index 0000000000..72b81e22a0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles-query-factory.util.ts @@ -0,0 +1,33 @@ +import gql from 'graphql-tag'; + +const DEFAULT_ROLE_GQL_FIELDS = ` + id + label + description + icon + isEditable + canUpdateAllSettings + canAccessAllTools + canReadAllObjectRecords + canUpdateAllObjectRecords + canSoftDeleteAllObjectRecords + canDestroyAllObjectRecords + canBeAssignedToUsers + canBeAssignedToAgents + canBeAssignedToApiKeys +`; + +export const findRolesQueryFactory = ({ + gqlFields = DEFAULT_ROLE_GQL_FIELDS, +}: { + gqlFields?: string; +} = {}) => ({ + query: gql` + query GetRoles { + getRoles { + ${gqlFields} + } + } + `, + variables: {}, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles.util.ts new file mode 100644 index 0000000000..cd1e907327 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/find-roles.util.ts @@ -0,0 +1,41 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { findRolesQueryFactory } from 'test/integration/metadata/suites/role/utils/find-roles-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.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 RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; + +export const findRoles = async ({ + gqlFields, + expectToFail, + token, +}: { + gqlFields?: string; + expectToFail?: boolean; + token?: string; +} = {}): CommonResponseBody<{ + getRoles: RoleDTO[]; +}> => { + const graphqlOperation = findRolesQueryFactory({ + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Role search should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Role search has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role-query-factory.util.ts new file mode 100644 index 0000000000..3c37d329e1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role-query-factory.util.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdateRolePayload } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; + +export type UpdateOneRoleFactoryInput = { + idToUpdate: string; + updatePayload: UpdateRolePayload; +}; + +const DEFAULT_ROLE_GQL_FIELDS = ` + id + label + description + icon + isEditable + canUpdateAllSettings + canAccessAllTools + canReadAllObjectRecords + canUpdateAllObjectRecords + canSoftDeleteAllObjectRecords + canDestroyAllObjectRecords + canBeAssignedToUsers + canBeAssignedToAgents + canBeAssignedToApiKeys +`; + +export const updateOneRoleQueryFactory = ({ + gqlFields = DEFAULT_ROLE_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdateOneRole($idToUpdate: UUID!, $updatePayload: UpdateRolePayload!) { + updateOneRole(updateRoleInput: {id: $idToUpdate, update: $updatePayload}) { + ${gqlFields} + } + } + `, + variables: { + idToUpdate: input.idToUpdate, + updatePayload: input.updatePayload, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role.util.ts new file mode 100644 index 0000000000..45329bf2c6 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-one-role.util.ts @@ -0,0 +1,42 @@ +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { + type UpdateOneRoleFactoryInput, + updateOneRoleQueryFactory, +} from 'test/integration/metadata/suites/role/utils/update-one-role-query-factory.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 RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; + +export const updateOneRole = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updateOneRole: RoleDTO; +}> => { + const graphqlOperation = updateOneRoleQueryFactory({ + input, + gqlFields, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Role update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Role update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role-query-factory.util.ts new file mode 100644 index 0000000000..44ee9d63e0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role-query-factory.util.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag'; + +export type UpdateWorkspaceMemberRoleInput = { + workspaceMemberId: string; + roleId: string; +}; + +const DEFAULT_WORKSPACE_MEMBER_GQL_FIELDS = ` + id + name { + firstName + lastName + } + userEmail + roles { + id + label + icon + } +`; + +export const updateWorkspaceMemberRoleQueryFactory = ({ + input, + gqlFields = DEFAULT_WORKSPACE_MEMBER_GQL_FIELDS, +}: { + input: UpdateWorkspaceMemberRoleInput; + gqlFields?: string; +}) => ({ + query: gql` + mutation UpdateWorkspaceMemberRole($workspaceMemberId: UUID!, $roleId: UUID!) { + updateWorkspaceMemberRole( + workspaceMemberId: $workspaceMemberId + roleId: $roleId + ) { + ${gqlFields} + } + } + `, + variables: { + workspaceMemberId: input.workspaceMemberId, + roleId: input.roleId, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role.util.ts b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role.util.ts new file mode 100644 index 0000000000..eb3192ceb7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/role/utils/update-workspace-member-role.util.ts @@ -0,0 +1,48 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { + type UpdateWorkspaceMemberRoleInput, + updateWorkspaceMemberRoleQueryFactory, +} from 'test/integration/metadata/suites/role/utils/update-workspace-member-role-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.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 WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; + +export const updateWorkspaceMemberRole = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: { + input: UpdateWorkspaceMemberRoleInput; + gqlFields?: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + updateWorkspaceMemberRole: WorkspaceMemberDTO; +}> => { + const graphqlOperation = updateWorkspaceMemberRoleQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Workspace member role update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Workspace member role update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap index e181b6bac8..1169682be3 100644 --- a/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap @@ -9,6 +9,8 @@ exports[`View Group REST API POST /metadata/viewGroups should fail to create vie "fieldMetadata": [], "index": [], "objectMetadata": [], + "role": [], + "roleTarget": [], "routeTrigger": [], "serverlessFunction": [], "view": [], @@ -40,6 +42,8 @@ exports[`View Group REST API POST /metadata/viewGroups should fail to create vie "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, diff --git a/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts index 87be8c8756..2bce0c89b8 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts @@ -240,7 +240,7 @@ describe('View Group REST API', () => { }); }); - describe.only('POST /metadata/viewGroups', () => { + describe('POST /metadata/viewGroups', () => { it('should create a new view group', async () => { const viewGroupData = { viewId: testViewId, @@ -450,7 +450,7 @@ describe('View Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorNotFoundResponse(deleteResponse2); + assertRestApiSuccessfulResponse(deleteResponse2); }); }); }); diff --git a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts index ba65f2c301..ac002623e3 100644 --- a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts +++ b/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts @@ -10,4 +10,6 @@ export const ALL_METADATA_NAME = { databaseEventTrigger: 'databaseEventTrigger', routeTrigger: 'routeTrigger', viewFilter: 'viewFilter', + role: 'role', + roleTarget: 'roleTarget', } as const;