mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 17:53:39 +00:00
[PM-28422] Client batching for member actions (#17805)
* add member action batching, feature flag, and implement batching for reinvite * clean up, fix tests, remove redundant tests * cleanup * clean up tests * bump cloud limit to 8k
This commit is contained in:
@@ -24,7 +24,7 @@ export const MaxCheckedCount = 500;
|
|||||||
* Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
|
* Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
|
||||||
* feature flag is enabled on cloud environments.
|
* feature flag is enabled on cloud environments.
|
||||||
*/
|
*/
|
||||||
export const CloudBulkReinviteLimit = 4000;
|
export const CloudBulkReinviteLimit = 8000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked).
|
* Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked).
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
try {
|
try {
|
||||||
const result = await this.memberActionsService.bulkReinvite(
|
const result = await this.memberActionsService.bulkReinvite(
|
||||||
organization,
|
organization,
|
||||||
filteredUsers.map((user) => user.id),
|
filteredUsers.map((user) => user.id as UserId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.successful) {
|
if (!result.successful) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { of } from "rxjs";
|
|||||||
import {
|
import {
|
||||||
OrganizationUserApiService,
|
OrganizationUserApiService,
|
||||||
OrganizationUserBulkResponse,
|
OrganizationUserBulkResponse,
|
||||||
|
OrganizationUserService,
|
||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
OrganizationUserType,
|
OrganizationUserType,
|
||||||
@@ -22,9 +23,8 @@ import { newGuid } from "@bitwarden/guid";
|
|||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||||
import { OrganizationUserService } from "../organization-user/organization-user.service";
|
|
||||||
|
|
||||||
import { MemberActionsService } from "./member-actions.service";
|
import { REQUESTS_PER_BATCH, MemberActionsService } from "./member-actions.service";
|
||||||
|
|
||||||
describe("MemberActionsService", () => {
|
describe("MemberActionsService", () => {
|
||||||
let service: MemberActionsService;
|
let service: MemberActionsService;
|
||||||
@@ -308,24 +308,32 @@ describe("MemberActionsService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("bulkReinvite", () => {
|
describe("bulkReinvite", () => {
|
||||||
const userIds = [newGuid(), newGuid(), newGuid()];
|
const userIds = [newGuid() as UserId, newGuid() as UserId, newGuid() as UserId];
|
||||||
|
|
||||||
|
describe("when feature flag is false", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
});
|
||||||
|
|
||||||
it("should successfully reinvite multiple users", async () => {
|
it("should successfully reinvite multiple users", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = new ListResponse(
|
||||||
|
{
|
||||||
data: userIds.map((id) => ({
|
data: userIds.map((id) => ({
|
||||||
id,
|
id,
|
||||||
error: null,
|
error: null,
|
||||||
})),
|
})),
|
||||||
continuationToken: null,
|
continuationToken: null,
|
||||||
} as ListResponse<OrganizationUserBulkResponse>;
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse);
|
organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
const result = await service.bulkReinvite(mockOrganization, userIds);
|
const result = await service.bulkReinvite(mockOrganization, userIds);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result.failed).toEqual([]);
|
||||||
successful: mockResponse,
|
expect(result.successful).toBeDefined();
|
||||||
failed: [],
|
expect(result.successful).toEqual(mockResponse);
|
||||||
});
|
|
||||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith(
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith(
|
||||||
organizationId,
|
organizationId,
|
||||||
userIds,
|
userIds,
|
||||||
@@ -346,6 +354,265 @@ describe("MemberActionsService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when feature flag is true (batching behavior)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
});
|
||||||
|
it("should process users in a single batch when count equals REQUESTS_PER_BATCH", async () => {
|
||||||
|
const userIdsBatch = Array.from({ length: REQUESTS_PER_BATCH }, () => newGuid() as UserId);
|
||||||
|
const mockResponse = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
expect(result.successful).toBeDefined();
|
||||||
|
expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH);
|
||||||
|
expect(result.failed).toHaveLength(0);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith(
|
||||||
|
organizationId,
|
||||||
|
userIdsBatch,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should process users in multiple batches when count exceeds REQUESTS_PER_BATCH", async () => {
|
||||||
|
const totalUsers = REQUESTS_PER_BATCH + 100;
|
||||||
|
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||||
|
|
||||||
|
const mockResponse1 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockResponse2 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite
|
||||||
|
.mockResolvedValueOnce(mockResponse1)
|
||||||
|
.mockResolvedValueOnce(mockResponse2);
|
||||||
|
|
||||||
|
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
expect(result.successful).toBeDefined();
|
||||||
|
expect(result.successful?.response).toHaveLength(totalUsers);
|
||||||
|
expect(result.failed).toHaveLength(0);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
organizationId,
|
||||||
|
userIdsBatch.slice(0, REQUESTS_PER_BATCH),
|
||||||
|
);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
organizationId,
|
||||||
|
userIdsBatch.slice(REQUESTS_PER_BATCH),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should aggregate results across multiple successful batches", async () => {
|
||||||
|
const totalUsers = REQUESTS_PER_BATCH + 50;
|
||||||
|
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||||
|
|
||||||
|
const mockResponse1 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockResponse2 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite
|
||||||
|
.mockResolvedValueOnce(mockResponse1)
|
||||||
|
.mockResolvedValueOnce(mockResponse2);
|
||||||
|
|
||||||
|
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
expect(result.successful).toBeDefined();
|
||||||
|
expect(result.successful?.response).toHaveLength(totalUsers);
|
||||||
|
expect(result.successful?.response.slice(0, REQUESTS_PER_BATCH)).toEqual(
|
||||||
|
mockResponse1.data,
|
||||||
|
);
|
||||||
|
expect(result.successful?.response.slice(REQUESTS_PER_BATCH)).toEqual(mockResponse2.data);
|
||||||
|
expect(result.failed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed individual errors across multiple batches", async () => {
|
||||||
|
const totalUsers = REQUESTS_PER_BATCH + 4;
|
||||||
|
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||||
|
|
||||||
|
const mockResponse1 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id, index) => ({
|
||||||
|
id,
|
||||||
|
error: index % 10 === 0 ? "Rate limit exceeded" : null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockResponse2 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
{ id: userIdsBatch[REQUESTS_PER_BATCH], error: null },
|
||||||
|
{ id: userIdsBatch[REQUESTS_PER_BATCH + 1], error: "Invalid email" },
|
||||||
|
{ id: userIdsBatch[REQUESTS_PER_BATCH + 2], error: null },
|
||||||
|
{ id: userIdsBatch[REQUESTS_PER_BATCH + 3], error: "User suspended" },
|
||||||
|
],
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite
|
||||||
|
.mockResolvedValueOnce(mockResponse1)
|
||||||
|
.mockResolvedValueOnce(mockResponse2);
|
||||||
|
|
||||||
|
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
// Count expected failures: every 10th index (0, 10, 20, ..., 490) in first batch + 2 explicit in second batch
|
||||||
|
// Indices 0 to REQUESTS_PER_BATCH-1 where index % 10 === 0: that's floor((BATCH_SIZE-1)/10) + 1 values
|
||||||
|
const expectedFailuresInBatch1 = Math.floor((REQUESTS_PER_BATCH - 1) / 10) + 1;
|
||||||
|
const expectedFailuresInBatch2 = 2;
|
||||||
|
const expectedTotalFailures = expectedFailuresInBatch1 + expectedFailuresInBatch2;
|
||||||
|
const expectedSuccesses = totalUsers - expectedTotalFailures;
|
||||||
|
|
||||||
|
expect(result.successful).toBeDefined();
|
||||||
|
expect(result.successful?.response).toHaveLength(expectedSuccesses);
|
||||||
|
expect(result.failed).toHaveLength(expectedTotalFailures);
|
||||||
|
expect(result.failed.some((f) => f.error === "Rate limit exceeded")).toBe(true);
|
||||||
|
expect(result.failed.some((f) => f.error === "Invalid email")).toBe(true);
|
||||||
|
expect(result.failed.some((f) => f.error === "User suspended")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should aggregate all failures when all batches fail", async () => {
|
||||||
|
const totalUsers = REQUESTS_PER_BATCH + 100;
|
||||||
|
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||||
|
const errorMessage = "All batches failed";
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite.mockRejectedValue(
|
||||||
|
new Error(errorMessage),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
expect(result.successful).toBeUndefined();
|
||||||
|
expect(result.failed).toHaveLength(totalUsers);
|
||||||
|
expect(result.failed.every((f) => f.error === errorMessage)).toBe(true);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty data in batch response", async () => {
|
||||||
|
const totalUsers = REQUESTS_PER_BATCH + 50;
|
||||||
|
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||||
|
|
||||||
|
const mockResponse1 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockResponse2 = new ListResponse(
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite
|
||||||
|
.mockResolvedValueOnce(mockResponse1)
|
||||||
|
.mockResolvedValueOnce(mockResponse2);
|
||||||
|
|
||||||
|
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
expect(result.successful).toBeDefined();
|
||||||
|
expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH);
|
||||||
|
expect(result.failed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should process batches sequentially in order", async () => {
|
||||||
|
const totalUsers = REQUESTS_PER_BATCH * 2;
|
||||||
|
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||||
|
const callOrder: number[] = [];
|
||||||
|
|
||||||
|
organizationUserApiService.postManyOrganizationUserReinvite.mockImplementation(
|
||||||
|
async (orgId, ids) => {
|
||||||
|
const batchIndex = ids.includes(userIdsBatch[0]) ? 1 : 2;
|
||||||
|
callOrder.push(batchIndex);
|
||||||
|
|
||||||
|
return new ListResponse(
|
||||||
|
{
|
||||||
|
data: ids.map((id) => ({
|
||||||
|
id,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
continuationToken: null,
|
||||||
|
},
|
||||||
|
OrganizationUserBulkResponse,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||||
|
|
||||||
|
expect(callOrder).toEqual([1, 2]);
|
||||||
|
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("allowResetPassword", () => {
|
describe("allowResetPassword", () => {
|
||||||
const resetPasswordEnabled = true;
|
const resetPasswordEnabled = true;
|
||||||
|
|
||||||
@@ -427,14 +694,6 @@ describe("MemberActionsService", () => {
|
|||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow reset password when organization lacks public and private keys", () => {
|
|
||||||
const org = { ...mockOrganization, hasPublicAndPrivateKeys: false } as Organization;
|
|
||||||
|
|
||||||
const result = service.allowResetPassword(mockOrgUser, org, resetPasswordEnabled);
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not allow reset password when user is not enrolled in reset password", () => {
|
it("should not allow reset password when user is not enrolled in reset password", () => {
|
||||||
const user = { ...mockOrgUser, resetPasswordEnrolled: false } as OrganizationUserView;
|
const user = { ...mockOrgUser, resetPasswordEnrolled: false } as OrganizationUserView;
|
||||||
|
|
||||||
@@ -443,12 +702,6 @@ describe("MemberActionsService", () => {
|
|||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow reset password when reset password is disabled", () => {
|
|
||||||
const result = service.allowResetPassword(mockOrgUser, mockOrganization, false);
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not allow reset password when user status is not confirmed", () => {
|
it("should not allow reset password when user status is not confirmed", () => {
|
||||||
const user = {
|
const user = {
|
||||||
...mockOrgUser,
|
...mockOrgUser,
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
|||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||||
|
|
||||||
|
export const REQUESTS_PER_BATCH = 500;
|
||||||
|
|
||||||
export interface MemberActionResult {
|
export interface MemberActionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -162,7 +165,13 @@ export class MemberActionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkReinvite(organization: Organization, userIds: string[]): Promise<BulkActionResult> {
|
async bulkReinvite(organization: Organization, userIds: UserId[]): Promise<BulkActionResult> {
|
||||||
|
const increaseBulkReinviteLimitForCloud = await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.IncreaseBulkReinviteLimitForCloud),
|
||||||
|
);
|
||||||
|
if (increaseBulkReinviteLimitForCloud) {
|
||||||
|
return await this.vNextBulkReinvite(organization, userIds);
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
const result = await this.organizationUserApiService.postManyOrganizationUserReinvite(
|
const result = await this.organizationUserApiService.postManyOrganizationUserReinvite(
|
||||||
organization.id,
|
organization.id,
|
||||||
@@ -175,6 +184,16 @@ export class MemberActionsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async vNextBulkReinvite(
|
||||||
|
organization: Organization,
|
||||||
|
userIds: UserId[],
|
||||||
|
): Promise<BulkActionResult> {
|
||||||
|
return this.processBatchedOperation(userIds, REQUESTS_PER_BATCH, (batch) =>
|
||||||
|
this.organizationUserApiService.postManyOrganizationUserReinvite(organization.id, batch),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
allowResetPassword(
|
allowResetPassword(
|
||||||
orgUser: OrganizationUserView,
|
orgUser: OrganizationUserView,
|
||||||
@@ -207,4 +226,52 @@ export class MemberActionsService {
|
|||||||
orgUser.status === OrganizationUserStatusType.Confirmed
|
orgUser.status === OrganizationUserStatusType.Confirmed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes user IDs in sequential batches and aggregates results.
|
||||||
|
* @param userIds - Array of user IDs to process
|
||||||
|
* @param batchSize - Number of IDs to process per batch
|
||||||
|
* @param processBatch - Async function that processes a single batch and returns the result
|
||||||
|
* @returns Aggregated bulk action result
|
||||||
|
*/
|
||||||
|
private async processBatchedOperation(
|
||||||
|
userIds: UserId[],
|
||||||
|
batchSize: number,
|
||||||
|
processBatch: (batch: string[]) => Promise<ListResponse<OrganizationUserBulkResponse>>,
|
||||||
|
): Promise<BulkActionResult> {
|
||||||
|
const allSuccessful: OrganizationUserBulkResponse[] = [];
|
||||||
|
const allFailed: { id: string; error: string }[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < userIds.length; i += batchSize) {
|
||||||
|
const batch = userIds.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await processBatch(batch);
|
||||||
|
|
||||||
|
if (result?.data) {
|
||||||
|
for (const response of result.data) {
|
||||||
|
if (response.error) {
|
||||||
|
allFailed.push({ id: response.id, error: response.error });
|
||||||
|
} else {
|
||||||
|
allSuccessful.push(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
allFailed.push(
|
||||||
|
...batch.map((id) => ({ id, error: (error as Error).message ?? String(error) })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successful =
|
||||||
|
allSuccessful.length > 0
|
||||||
|
? new ListResponse(allSuccessful, OrganizationUserBulkResponse)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
successful,
|
||||||
|
failed: allFailed,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user