Fix seeding perf + batch role targets creation (#16337)

This commit is contained in:
Weiko
2025-12-04 20:29:48 +01:00
committed by GitHub
parent e653385485
commit 1991ee850e
11 changed files with 156 additions and 110 deletions
@@ -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(
@@ -157,9 +157,9 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
);
}
await this.userRoleService.assignRoleToUserWorkspace({
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId: workspace.id,
userWorkspaceId: userWorkspace.id,
userWorkspaceIds: [userWorkspace.id],
roleId: defaultRoleId,
});
@@ -37,6 +37,25 @@ export class RoleTargetService {
createRoleTargetInput: CreateRoleTargetInput;
workspaceId: string;
}): Promise<FlatRoleTarget> {
const [flatRoleTarget] = await this.createMany({
createRoleTargetInputs: [createRoleTargetInput],
workspaceId,
});
return flatRoleTarget;
}
async createMany({
createRoleTargetInputs,
workspaceId,
}: {
createRoleTargetInputs: CreateRoleTargetInput[];
workspaceId: string;
}): Promise<FlatRoleTarget[]> {
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<void> {
@@ -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,
});
@@ -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,
});
}
}
@@ -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<void> {
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<string[]> {
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({
@@ -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,
});
}
}
@@ -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,
});
}
}
@@ -89,5 +89,10 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
flatPageLayoutTabMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
};
};
@@ -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,
});
}
@@ -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,