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 97b05448e5..517f164302 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 @@ -110,7 +110,7 @@ describe('UserWorkspaceService', () => { { provide: UserRoleService, useValue: { - assignRoleToUserWorkspace: jest.fn(), + assignRoleToManyUserWorkspace: jest.fn(), }, }, { @@ -417,7 +417,7 @@ describe('UserWorkspaceService', () => { jest.spyOn(service, 'create').mockResolvedValue(userWorkspace); jest.spyOn(service, 'createWorkspaceMember').mockResolvedValue(undefined); jest - .spyOn(userRoleService, 'assignRoleToUserWorkspace') + .spyOn(userRoleService, 'assignRoleToManyUserWorkspace') .mockResolvedValue(undefined); jest .spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation') @@ -439,9 +439,11 @@ describe('UserWorkspaceService', () => { workspace.id, user, ); - expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({ + expect( + userRoleService.assignRoleToManyUserWorkspace, + ).toHaveBeenCalledWith({ workspaceId: workspace.id, - userWorkspaceId: userWorkspace.id, + userWorkspaceIds: [userWorkspace.id], roleId: workspace.defaultRoleId, }); expect( 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 6886b0d921..5a407ec56a 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 @@ -157,9 +157,9 @@ export class UserWorkspaceService extends TypeOrmQueryService { + const [flatRoleTarget] = await this.createMany({ + createRoleTargetInputs: [createRoleTargetInput], + workspaceId, + }); + + return flatRoleTarget; + } + + async createMany({ + createRoleTargetInputs, + workspaceId, + }: { + createRoleTargetInputs: CreateRoleTargetInput[]; + workspaceId: string; + }): Promise { + if (createRoleTargetInputs.length === 0) { + return []; + } + const { flatRoleTargetMaps, flatRoleMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -44,6 +63,7 @@ export class RoleTargetService { flatMapsKeys: ['flatRoleTargetMaps', 'flatRoleMaps'], }, ); + const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -51,17 +71,25 @@ export class RoleTargetService { }, ); - const { flatRoleTargetToCreate, flatRoleTargetsToDelete } = - fromCreateRoleTargetInputToFlatRoleTargetToCreate({ - createRoleTargetInput: { - ...createRoleTargetInput, - applicationId: - createRoleTargetInput.applicationId ?? - workspaceCustomFlatApplication.id, - }, - flatRoleTargetMaps, - workspaceId, - }); + const allFlatRoleTargetsToCreate: FlatRoleTarget[] = []; + const allFlatRoleTargetsToDelete: FlatRoleTarget[] = []; + + for (const createRoleTargetInput of createRoleTargetInputs) { + const { flatRoleTargetToCreate, flatRoleTargetsToDelete } = + fromCreateRoleTargetInputToFlatRoleTargetToCreate({ + createRoleTargetInput: { + ...createRoleTargetInput, + applicationId: + createRoleTargetInput.applicationId ?? + workspaceCustomFlatApplication.id, + }, + flatRoleTargetMaps, + workspaceId, + }); + + allFlatRoleTargetsToCreate.push(flatRoleTargetToCreate); + allFlatRoleTargetsToDelete.push(...flatRoleTargetsToDelete); + } const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( @@ -69,8 +97,8 @@ export class RoleTargetService { fromToAllFlatEntityMaps: { flatRoleTargetMaps: computeFlatEntityMapsFromTo({ flatEntityMaps: flatRoleTargetMaps, - flatEntityToCreate: [flatRoleTargetToCreate], - flatEntityToDelete: flatRoleTargetsToDelete, + flatEntityToCreate: allFlatRoleTargetsToCreate, + flatEntityToDelete: allFlatRoleTargetsToDelete, flatEntityToUpdate: [], }), }, @@ -90,7 +118,7 @@ export class RoleTargetService { if (isDefined(validateAndBuildResult)) { throw new WorkspaceMigrationBuilderExceptionV2( validateAndBuildResult, - 'Multiple validation errors occurred while creating role target', + 'Multiple validation errors occurred while creating role targets', ); } @@ -102,10 +130,12 @@ export class RoleTargetService { }, ); - return findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: flatRoleTargetToCreate.id, - flatEntityMaps: recomputedFlatRoleTargetMaps, - }); + return allFlatRoleTargetsToCreate.map((flatRoleTargetToCreate) => + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatRoleTargetToCreate.id, + flatEntityMaps: recomputedFlatRoleTargetMaps, + }), + ); } async delete({ id, workspaceId }: DeleteRoleTargetInput): Promise { 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 6e5d578cc3..c58aea184b 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 @@ -126,8 +126,8 @@ export class RoleResolver { workspaceId: workspace.id, }); - await this.userRoleService.assignRoleToUserWorkspace({ - userWorkspaceId: userWorkspace.id, + await this.userRoleService.assignRoleToManyUserWorkspace({ + userWorkspaceIds: [userWorkspace.id], workspaceId: workspace.id, roleId, }); 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 fe3dd11d12..f8111a0048 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 @@ -354,14 +354,10 @@ export class RoleService { workspaceId, ); - await Promise.all( - userWorkspaceIds.map((userWorkspaceId) => - this.userRoleService.assignRoleToUserWorkspace({ - userWorkspaceId, - roleId: defaultRoleId, - workspaceId, - }), - ), - ); + await this.userRoleService.assignRoleToManyUserWorkspace({ + userWorkspaceIds, + roleId: defaultRoleId, + workspaceId, + }); } } 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 00454df406..a96087cabe 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 @@ -29,31 +29,38 @@ export class UserRoleService { private readonly workspaceCacheService: WorkspaceCacheService, ) {} - public async assignRoleToUserWorkspace({ + public async assignRoleToManyUserWorkspace({ workspaceId, - userWorkspaceId, + userWorkspaceIds, roleId, }: { workspaceId: string; - userWorkspaceId: string; + userWorkspaceIds: string[]; roleId: string; }): Promise { - const validationResult = await this.validateAssignRoleInput({ - userWorkspaceId, - workspaceId, - roleId, - }); - - if (validationResult?.roleToAssignIsSameAsCurrentRole) { + if (userWorkspaceIds.length === 0) { return; } - await this.roleTargetService.create({ - createRoleTargetInput: { + const userWorkspaceIdsToAssign = + await this.validateAssignRoleInputsAndGetUserWorkspaceIdsToAssign({ + userWorkspaceIds, + workspaceId, roleId, - targetId: userWorkspaceId, - targetMetadataForeignKey: 'userWorkspaceId', - }, + }); + + if (userWorkspaceIdsToAssign.length === 0) { + return; + } + + await this.roleTargetService.createMany({ + createRoleTargetInputs: userWorkspaceIdsToAssign.map( + (userWorkspaceId) => ({ + roleId, + targetId: userWorkspaceId, + targetMetadataForeignKey: 'userWorkspaceId' as const, + }), + ), workspaceId, }); } @@ -208,57 +215,72 @@ export class UserRoleService { } } - private async validateAssignRoleInput({ - userWorkspaceId, + private async validateAssignRoleInputsAndGetUserWorkspaceIdsToAssign({ + userWorkspaceIds, workspaceId, roleId, }: { - userWorkspaceId: string; + userWorkspaceIds: string[]; workspaceId: string; roleId: string; - }) { - const userWorkspace = await this.userWorkspaceRepository.findOne({ + }): Promise { + const userWorkspaces = await this.userWorkspaceRepository.find({ where: { - id: userWorkspaceId, + id: In(userWorkspaceIds), }, }); - if (!isDefined(userWorkspace)) { + const foundUserWorkspaceIds = new Set( + userWorkspaces.map((userWorkspace) => userWorkspace.id), + ); + + const missingUserWorkspaceIds = userWorkspaceIds.filter( + (id) => !foundUserWorkspaceIds.has(id), + ); + + if (missingUserWorkspaceIds.length > 0) { throw new PermissionsException( - 'User workspace not found', + `User workspaces not found: ${missingUserWorkspaceIds.join(', ')}`, PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND, { - userFriendlyMessage: msg`Your workspace membership could not be found. You may no longer have access to this workspace.`, + userFriendlyMessage: msg`Some workspace memberships could not be found. They may no longer have access to this workspace.`, }, ); } - const roles = await this.getRolesByUserWorkspaces({ - userWorkspaceIds: [userWorkspace.id], + const rolesByUserWorkspaces = await this.getRolesByUserWorkspaces({ + userWorkspaceIds, workspaceId, }); - const currentRole = roles.get(userWorkspace.id)?.[0]; + const userWorkspaceIdsToAssign: string[] = []; + let adminRoleIdToValidate: string | undefined; - if (currentRole?.id === roleId) { - return { - roleToAssignIsSameAsCurrentRole: true, - }; - } + for (const userWorkspaceId of userWorkspaceIds) { + const currentRole = rolesByUserWorkspaces.get(userWorkspaceId)?.[0]; - if ( - !( + if (currentRole?.id === roleId) { + continue; + } + + if ( isDefined(currentRole) && currentRole.standardId === ADMIN_ROLE.standardId - ) - ) { - return; + ) { + adminRoleIdToValidate = currentRole.id; + } + + userWorkspaceIdsToAssign.push(userWorkspaceId); } - await this.validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({ - workspaceId, - adminRoleId: currentRole.id, - }); + if (isDefined(adminRoleIdToValidate)) { + await this.validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({ + workspaceId, + adminRoleId: adminRoleIdToValidate, + }); + } + + return userWorkspaceIdsToAssign; } private async validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({ 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 bf03a9bba4..97b8092463 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 @@ -99,9 +99,9 @@ export class DevSeederPermissionsService { applicationId: twentyStandardApplication.id, }); - await this.userRoleService.assignRoleToUserWorkspace({ + await this.userRoleService.assignRoleToManyUserWorkspace({ workspaceId, - userWorkspaceId: guestUserWorkspaceId, + userWorkspaceIds: [guestUserWorkspaceId], roleId: guestRole.id, }); @@ -110,9 +110,9 @@ export class DevSeederPermissionsService { applicationId: twentyStandardApplication.id, }); - await this.userRoleService.assignRoleToUserWorkspace({ + await this.userRoleService.assignRoleToManyUserWorkspace({ workspaceId, - userWorkspaceId: limitedUserWorkspaceId, + userWorkspaceIds: [limitedUserWorkspaceId], roleId: limitedRole.id, }); } else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) { @@ -125,9 +125,9 @@ export class DevSeederPermissionsService { } if (adminUserWorkspaceId) { - await this.userRoleService.assignRoleToUserWorkspace({ + await this.userRoleService.assignRoleToManyUserWorkspace({ workspaceId, - userWorkspaceId: adminUserWorkspaceId, + userWorkspaceIds: [adminUserWorkspaceId], roleId: adminRole.id, }); } @@ -144,14 +144,12 @@ export class DevSeederPermissionsService { activationStatus: WorkspaceActivationStatus.ACTIVE, }); - if (memberUserWorkspaceIds) { - for (const memberUserWorkspaceId of memberUserWorkspaceIds) { - await this.userRoleService.assignRoleToUserWorkspace({ - workspaceId, - userWorkspaceId: memberUserWorkspaceId, - roleId: memberRole.id, - }); - } + if (memberUserWorkspaceIds.length > 0) { + await this.userRoleService.assignRoleToManyUserWorkspace({ + workspaceId, + userWorkspaceIds: memberUserWorkspaceIds, + roleId: memberRole.id, + }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts index bbc40dee35..f9c5936120 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts @@ -9,7 +9,6 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @@ -36,7 +35,6 @@ export class DevSeederService { private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService, private readonly devSeederMetadataService: DevSeederMetadataService, private readonly devSeederPermissionsService: DevSeederPermissionsService, - private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly devSeederDataService: DevSeederDataService, private readonly applicationService: ApplicationService, private readonly workspaceCacheService: WorkspaceCacheService, @@ -61,9 +59,10 @@ export class DevSeederService { workspaceId, ); - await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ - 'flatApplicationMaps', - ]); + const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute( + workspaceId, + ['flatApplicationMaps', 'featureFlagsMap'], + ); const dataSourceMetadata = await this.dataSourceService.createDataSourceMetadata( @@ -71,9 +70,6 @@ export class DevSeederService { schemaName, ); - const featureFlags = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); - const twentyStandardApplication = await this.applicationService.findByUniversalIdentifier({ workspaceId, @@ -96,13 +92,13 @@ export class DevSeederService { await this.workspaceSyncMetadataService.synchronize({ workspaceId: workspaceId, dataSourceId: dataSourceMetadata.id, - featureFlags, + featureFlags: featureFlagsMap, }); await this.devSeederMetadataService.seed({ dataSourceMetadata, workspaceId, - featureFlags, + featureFlags: featureFlagsMap, twentyStandardFlatApplication, }); @@ -130,10 +126,8 @@ export class DevSeederService { relations: { fields: true }, }); - const isDashboardV2Enabled = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_DASHBOARD_V2_ENABLED, - workspaceId, - ); + const isDashboardV2Enabled = + featureFlagsMap[FeatureFlagKey.IS_DASHBOARD_V2_ENABLED] ?? false; await seedPageLayoutWidgets({ dataSource: this.coreDataSource, @@ -146,12 +140,9 @@ export class DevSeederService { await this.devSeederDataService.seed({ schemaName: dataSourceMetadata.schema, workspaceId, - featureFlags, + featureFlags: featureFlagsMap, }); await this.workspaceCacheStorageService.flush(workspaceId, undefined); - await this.flatEntityMapsCacheService.flushFlatEntityMaps({ - workspaceId, - }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts index 379ef276c4..c90f761eca 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts @@ -89,5 +89,10 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({ idByUniversalIdentifier: {}, universalIdentifiersByApplicationId: {}, }, + flatPageLayoutTabMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index cff22d5df6..5a8a767cdc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -201,9 +201,9 @@ export class WorkspaceManagerService { where: { workspaceId, userId }, }); - await this.userRoleService.assignRoleToUserWorkspace({ + await this.userRoleService.assignRoleToManyUserWorkspace({ workspaceId, - userWorkspaceId: userWorkspace.id, + userWorkspaceIds: [userWorkspace.id], roleId: adminRole.id, }); } 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 3c06f51ec2..918516169c 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 @@ -29,6 +29,7 @@ exports[`Object metadata update should fail when labelIdentifier is a UUID field "type": "update_object", }, ], + "pageLayoutTab": [], "role": [], "roleTarget": [], "routeTrigger": [], @@ -63,6 +64,7 @@ exports[`Object metadata update should fail when labelIdentifier is a UUID field "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, + "invalidPageLayoutTab": 0, "invalidRole": 0, "invalidRoleTarget": 0, "invalidRouteTrigger": 0,