Fix seeding perf + batch role targets creation (#16337)
This commit is contained in:
+6
-4
@@ -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(
|
||||
|
||||
+2
-2
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+48
-18
@@ -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({
|
||||
|
||||
+12
-14
@@ -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
-18
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user