Skip to content

Batch item rest requests #19233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
540d7e1
wip batch item requests
madsrasmussen May 1, 2025
854d10a
move batching logic to logic implementations
madsrasmussen May 2, 2025
931cb8e
testing different implementations
madsrasmussen May 2, 2025
b203688
move batch array function to utils
madsrasmussen May 2, 2025
f7b23fc
return an error if there are any
madsrasmussen May 2, 2025
45cc5fc
split function
madsrasmussen May 2, 2025
0b4b24a
wip testing implementations
madsrasmussen May 2, 2025
2ba0d4b
move batch-try-execute
madsrasmussen May 5, 2025
a42ed5e
clean up
madsrasmussen May 5, 2025
f26d3f2
fix response typing
madsrasmussen May 5, 2025
dfcbce8
rename to controller
madsrasmussen May 5, 2025
3991d0b
disable notifications in tryExecute
madsrasmussen May 5, 2025
ca93553
handle error
madsrasmussen May 5, 2025
5ead180
add interface for responses with data
madsrasmussen May 5, 2025
ef663b4
Update media-item.server.data-source.ts
madsrasmussen May 5, 2025
78995d3
align naming
madsrasmussen May 5, 2025
cd05e2f
move to entity-item module
madsrasmussen May 5, 2025
8a14463
extend controller base
madsrasmussen May 5, 2025
0a7cd2b
implement across
madsrasmussen May 5, 2025
5a0275d
update name
madsrasmussen May 5, 2025
259ed81
update file name
madsrasmussen May 5, 2025
2276169
Update index.ts
madsrasmussen May 5, 2025
dc722d3
increase batch size
madsrasmussen May 5, 2025
45148f2
Merge branch 'release/16.0' into v16/hotfix/batch-item-rest-requests
madsrasmussen May 5, 2025
1006ee4
Merge branch 'release/16.0' into v16/hotfix/batch-item-rest-requests
madsrasmussen May 7, 2025
dbe5d45
Update member-group-item.server.data-source.ts
madsrasmussen May 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './item-data-api-get-request-controller/index.js';
export * from './entity-item-ref/index.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './item-data-api-get-request.controller.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { UmbItemDataApiGetRequestControllerArgs } from './types.js';
import {
batchTryExecute,
tryExecute,
UmbError,
type UmbApiError,
type UmbCancelError,
type UmbDataApiResponse,
} from '@umbraco-cms/backoffice/resources';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { batchArray } from '@umbraco-cms/backoffice/utils';
import { umbPeekError } from '@umbraco-cms/backoffice/notification';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';

export class UmbItemDataApiGetRequestController<
ResponseModelType extends UmbDataApiResponse,
> extends UmbControllerBase {
#apiCallback: (args: { uniques: Array<string> }) => Promise<ResponseModelType>;
#uniques: Array<string>;
#batchSize: number = 40;

constructor(host: UmbControllerHost, args: UmbItemDataApiGetRequestControllerArgs<ResponseModelType>) {
super(host);
this.#apiCallback = args.api;
this.#uniques = args.uniques;
}

async request() {
if (!this.#uniques) throw new Error('Uniques are missing');

let data: ResponseModelType['data'] | undefined;
let error: UmbError | UmbApiError | UmbCancelError | Error | undefined;

if (this.#uniques.length > this.#batchSize) {
const chunks = batchArray<string>(this.#uniques, this.#batchSize);
const results = await batchTryExecute(this, chunks, (chunk) => this.#apiCallback({ uniques: chunk }));

const errors = results.filter((promiseResult) => promiseResult.status === 'rejected');

if (errors.length > 0) {
error = await this.#getAndHandleErrorResult(errors);
}

data = results
.filter((promiseResult) => promiseResult.status === 'fulfilled')
.flatMap((promiseResult) => promiseResult.value.data);
} else {
const result = await tryExecute(this, this.#apiCallback({ uniques: this.#uniques }));
data = result.data;
error = result.error;
}

return { data, error };
}

async #getAndHandleErrorResult(errors: Array<PromiseRejectedResult>) {
// TODO: We currently expect all the errors to be the same, but we should handle this better in the future.
const error = errors[0];
await umbPeekError(this, {
headline: 'Error fetching items',
message: 'An error occurred while fetching items.',
});

return new UmbError(error.reason);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { UmbDataApiResponse } from '@umbraco-cms/backoffice/resources';

export interface UmbItemDataApiGetRequestControllerArgs<ResponseModelType extends UmbDataApiResponse> {
api: (args: { uniques: Array<string> }) => Promise<ResponseModelType>;
uniques: Array<string>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity';
export type * from './item-data-api-get-request-controller/types.js';

export interface UmbDefaultItemModel extends UmbNamedEntityModel {
icon?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbDataSourceResponse } from '../data-source-response.interface.js';
import type { UmbItemDataSource } from './item-data-source.interface.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';

export interface UmbItemServerDataSourceBaseArgs<ServerItemType, ClientItemType extends { unique: string }> {
getItems: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
getItems?: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
mapper: (item: ServerItemType) => ClientItemType;
}

Expand All @@ -14,10 +15,10 @@ export interface UmbItemServerDataSourceBaseArgs<ServerItemType, ClientItemType
* @implements {DocumentTreeDataSource}
*/
export abstract class UmbItemServerDataSourceBase<ServerItemType, ClientItemType extends { unique: string }>
extends UmbControllerBase
implements UmbItemDataSource<ClientItemType>
{
#host: UmbControllerHost;
#getItems: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
#getItems?: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
#mapper: (item: ServerItemType) => ClientItemType;

/**
Expand All @@ -27,7 +28,7 @@ export abstract class UmbItemServerDataSourceBase<ServerItemType, ClientItemType
* @memberof UmbItemServerDataSourceBase
*/
constructor(host: UmbControllerHost, args: UmbItemServerDataSourceBaseArgs<ServerItemType, ClientItemType>) {
this.#host = host;
super(host);
this.#getItems = args.getItems;
this.#mapper = args.mapper;
}
Expand All @@ -39,14 +40,17 @@ export abstract class UmbItemServerDataSourceBase<ServerItemType, ClientItemType
* @memberof UmbItemServerDataSourceBase
*/
async getItems(uniques: Array<string>) {
if (!this.#getItems) throw new Error('getItems is not implemented');
if (!uniques) throw new Error('Uniques are missing');
const { data, error } = await tryExecute(this.#host, this.#getItems(uniques));

if (data) {
const items = data.map((item) => this.#mapper(item));
return { data: items };
}
const { data, error } = await tryExecute(this, this.#getItems(uniques));

return { error };
return { data: this._getMappedItems(data), error };
}

protected _getMappedItems(items: Array<ServerItemType> | undefined): Array<ClientItemType> | undefined {
if (!items) return undefined;
if (!this.#mapper) throw new Error('Mapper is not implemented');
return items.map((item) => this.#mapper(item));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface UmbDataApiResponse<ResponseType extends { data: unknown } = { data: unknown }> {
data: ResponseType['data'];
}
11 changes: 4 additions & 7 deletions src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
export * from './api-interceptor.controller.js';
export * from './resource.controller.js';
export * from './try-execute.controller.js';
export * from './tryExecute.function.js';
export * from './tryExecuteAndNotify.function.js';
export * from './tryXhrRequest.function.js';
export * from './extractUmbNotificationColor.function.js';
export * from './apiTypeValidators.function.js';
export * from './extractUmbColorVariable.function.js';
export * from './extractUmbNotificationColor.function.js';
export * from './isUmbNotifications.function.js';
export * from './apiTypeValidators.function.js';
export * from './resource.controller.js';
export * from './try-execute/index.js';
export * from './umb-error.js';
export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { tryExecute } from './tryExecute.function.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';

/**
* Batches promises and returns a promise that resolves to an array of results
* @param {UmbControllerHost} host - The host to use for the request and where notifications will be shown
* @param {Array<Array<BatchEntryType>>} chunks - The array of chunks to process
* @param {(chunk: Array<BatchEntryType>) => Promise<PromiseResult>} callback - The function to call for each chunk
* @returns {Promise<PromiseSettledResult<PromiseResult>[]>} - A promise that resolves to an array of results
*/
export function batchTryExecute<BatchEntryType, PromiseResult>(
host: UmbControllerHost,
chunks: Array<Array<BatchEntryType>>,
callback: (chunk: Array<BatchEntryType>) => Promise<PromiseResult>,
): Promise<PromiseSettledResult<PromiseResult>[]> {
return Promise.allSettled(chunks.map((chunk) => tryExecute(host, callback(chunk), { disableNotifications: true })));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './batch-try-execute.function.js';
export * from './try-execute.controller.js';
export * from './tryExecute.function.js';
export * from './tryExecuteAndNotify.function.js';
export * from './tryXhrRequest.function.js';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isProblemDetailsLike } from './apiTypeValidators.function.js';
import { UmbResourceController } from './resource.controller.js';
import type { UmbApiResponse, UmbTryExecuteOptions } from './types.js';
import { UmbApiError, UmbCancelError } from './umb-error.js';
import { isProblemDetailsLike } from '../apiTypeValidators.function.js';
import { UmbResourceController } from '../resource.controller.js';
import type { UmbApiResponse, UmbTryExecuteOptions } from '../types.js';
import { UmbApiError, UmbCancelError } from '../umb-error.js';

export class UmbTryExecuteController<T> extends UmbResourceController<T> {
#abortSignal?: AbortSignal;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UmbApiResponse, UmbTryExecuteOptions } from '../types.js';
import { UmbTryExecuteController } from './try-execute.controller.js';
import type { UmbApiResponse, UmbTryExecuteOptions } from './types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UmbApiResponse } from '../types.js';
import { UmbTryExecuteController } from './try-execute.controller.js';
import type { UmbApiResponse } from './types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDeprecation } from '@umbraco-cms/backoffice/utils';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { UmbCancelablePromise } from '../cancelable-promise.js';
import { UmbApiError } from '../umb-error.js';
import { isProblemDetailsLike } from '../apiTypeValidators.function.js';
import type { UmbApiResponse, XhrRequestOptions } from '../types.js';
import { UmbTryExecuteController } from './try-execute.controller.js';
import { UmbCancelablePromise } from './cancelable-promise.js';
import { UmbApiError } from './umb-error.js';
import { isProblemDetailsLike } from './apiTypeValidators.function.js';
import type { UmbApiResponse, XhrRequestOptions } from './types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { umbHttpClient } from '@umbraco-cms/backoffice/http-client';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UmbApiError, UmbCancelError, UmbError } from './umb-error.js';
export type * from './data-api/types.js';

export interface XhrRequestOptions extends UmbTryExecuteOptions {
baseUrl?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect } from '@open-wc/testing';
import { batchArray } from './batch-array.js';

describe('batchArray', () => {
it('should split an array into chunks of the specified size', () => {
const array = [1, 2, 3, 4, 5];
const batchSize = 2;
const result = batchArray(array, batchSize);
expect(result).to.deep.equal([[1, 2], [3, 4], [5]]);
});

it('should handle arrays smaller than the batch size', () => {
const array = [1];
const batchSize = 2;
const result = batchArray(array, batchSize);
expect(result).to.deep.equal([[1]]);
});

it('should handle empty arrays', () => {
const array: number[] = [];
const batchSize = 2;
const result = batchArray(array, batchSize);
expect(result).to.deep.equal([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Splits an array into chunks of a specified size
* @param { Array<BatchEntryType> } array - The array to split
* @param {number }batchSize - The size of each chunk
* @returns {Array<Array<T>>} - An array of chunks
*/
export function batchArray<BatchEntryType>(
array: Array<BatchEntryType>,
batchSize: number,
): Array<Array<BatchEntryType>> {
const chunks: Array<Array<BatchEntryType>> = [];
for (let i = 0; i < array.length; i += batchSize) {
chunks.push(array.slice(i, i + batchSize));
}
return chunks;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './batch-array.js';
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './array/index.js';
export * from './bytes/bytes.function.js';
export * from './debounce/debounce.function.js';
export * from './deprecation/index.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DataTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';
import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item';

let manifestPropertyEditorUis: Array<ManifestPropertyEditorUi> = [];

Expand All @@ -26,7 +27,6 @@ export class UmbDataTypeItemServerDataSource extends UmbItemServerDataSourceBase

constructor(host: UmbControllerHost) {
super(host, {
getItems,
mapper,
});

Expand All @@ -37,10 +37,21 @@ export class UmbDataTypeItemServerDataSource extends UmbItemServerDataSourceBase
})
.unsubscribe();
}
}

/* eslint-disable local-rules/no-direct-api-import */
const getItems = (uniques: Array<string>) => DataTypeService.getItemDataType({ query: { id: uniques } });
override async getItems(uniques: Array<string>) {
if (!uniques) throw new Error('Uniques are missing');

const itemRequestManager = new UmbItemDataApiGetRequestController(this, {
// eslint-disable-next-line local-rules/no-direct-api-import
api: (args) => DataTypeService.getItemDataType({ query: { id: args.uniques } }),
uniques,
});

const { data, error } = await itemRequestManager.request();

return { data: this._getMappedItems(data), error };
}
}

const mapper = (item: DataTypeItemResponseModel): UmbDataTypeItemModel => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'
import type { DictionaryItemItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { DictionaryService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item';

/**
* A server data source for Dictionary items
Expand All @@ -21,14 +22,24 @@ export class UmbDictionaryItemServerDataSource extends UmbItemServerDataSourceBa
*/
constructor(host: UmbControllerHost) {
super(host, {
getItems,
mapper,
});
}
}

/* eslint-disable local-rules/no-direct-api-import */
const getItems = (uniques: Array<string>) => DictionaryService.getItemDictionary({ query: { id: uniques } });
override async getItems(uniques: Array<string>) {
if (!uniques) throw new Error('Uniques are missing');

const itemRequestManager = new UmbItemDataApiGetRequestController(this, {
// eslint-disable-next-line local-rules/no-direct-api-import
api: (args) => DictionaryService.getItemDictionary({ query: { id: args.uniques } }),
uniques,
});

const { data, error } = await itemRequestManager.request();

return { data: this._getMappedItems(data), error };
}
}

const mapper = (item: DictionaryItemItemResponseModel): UmbDictionaryItemModel => {
return {
Expand Down
Loading
Loading