@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
169 lines (166 loc) • 7.54 kB
JavaScript
/**
* @package @bitrix24/b24jssdk
* @version 3.0.0
* @copyright (c) 2026 Bitrix24
* @license MIT
* @see https://github.com/bitrix24/b24jssdk
* @see https://bitrix24.github.io/b24jssdk/
*/
import { resolveMaxPages, assertNotAborted, maxPagesExceededError } from '../_walk-bounds.mjs';
import { AbstractAction } from '../abstract-action.mjs';
import { SdkError } from '../../sdk-error.mjs';
import { cursorStalledError, cursorWentBackwardsError, CURSOR_STALLED_HINT_LIST } from '../_cursor-stalled.mjs';
import { cursorProgressed } from '../_cursor-progress.mjs';
import { warnOnShadowedUppercaseParams } from './_uppercase-list-params.mjs';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
class FetchListV2 extends AbstractAction {
static {
__name(this, "FetchListV2");
}
/**
* Calls a REST API list method and returns an async generator, for walking a
* large dataset without holding all of it in memory.
*
* **Every option is documented on the [fetchList v2 page](https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/fetch-list-rest-api-ver2/),
* and each one on {@link ActionFetchListV2}.** Not repeated here: nothing
* watches a sentence in a comment, while the page is link-checked and its
* code compiled on every CI run.
*
* What matters while editing this file:
*
* - The cursor only advances if rows arrive sorted by `cursorIdKey` ascending,
* so the walk writes its own `order` and strips a caller's with a `warning`.
* - `idKey` reads the RESPONSE, `cursorIdKey` writes the REQUEST, and the two
* fail differently. A wrong `cursorIdKey` means the page condition never
* matches, the same page keeps arriving, and the walk stops with
* {@link cursorStalledError}. A wrong `idKey` is quieter: if the value
* cannot be read as a number the walk warns and stops short, and if it
* names a *different numeric* field it advances a cursor the request never
* sorts by — which skips rows rather than reporting anything.
* - Page size on `restApi:v2` is a fixed 50 — there is no `limit` to ask with,
* so a page shorter than that ends the walk, but only once the cursor read
* from it has been vouched for.
* - The portal keeps only the later of two top-level keys differing by case,
* so a caller's uppercase `FILTER` can drop either their conditions or this
* walk's cursor. Both are warned about, neither can be fixed from in here —
* see {@link warnOnShadowedUppercaseParams}.
* - `maxPages` never ends a walk silently. Every page up to the ceiling has
* already been yielded and is the consumer's; the throw is what stops a
* truncated walk from reading as a finished one.
*
* @template T - The type of items in the returned arrays (default is `unknown`).
* @param {ActionFetchListV2} options - every field is documented on the type.
* @returns {AsyncGenerator<T[]>} An async generator yielding one page of rows
* at a time until the dataset is exhausted.
*
* @example
* import { EnumCrmEntityTypeId, Text } from '@bitrix24/b24jssdk'
*
* interface CrmItem { id: number, title: string }
* const sixMonthAgo = new Date()
* sixMonthAgo.setMonth((new Date()).getMonth() - 6)
* sixMonthAgo.setHours(0, 0, 0)
* const generator = b24.actions.v2.fetchList.make<CrmItem>({
* method: 'crm.item.list',
* params: {
* entityTypeId: EnumCrmEntityTypeId.company,
* filter: {
* '=%title': 'A%',
* '>=createdTime': Text.toB24Format(sixMonthAgo) // created at least 6 months ago
* },
* select: ['id', 'title']
* },
* idKey: 'id',
* customKeyForResult: 'items',
* requestId: 'list-123'
* })
*
* for await (const chunk of generator) {
* // Process chunk (e.g., save to database, analyze, etc.)
* console.log(`Processing ${chunk.length} items`)
* }
*
* @see {@link https://apidocs.bitrix24.com/settings/performance/huge-data.html Bitrix24: Fast algorithm for large data}
*/
async *make(options) {
const batchSize = 50;
const idKey = options?.idKey ?? "ID";
const cursorIdKey = options?.cursorIdKey ?? idKey;
const customKeyForResult = options?.customKeyForResult ?? null;
const params = options?.params ?? {};
if ("order" in params && params["order"]) {
this._logger.warning("fetchList.make: user-provided `order` parameter is ignored because cursor-based pagination requires ordering by cursorIdKey. Use `filter` to narrow results instead.").catch(() => {
});
}
const moreIdKey = `>${cursorIdKey}`;
const { order: _ignoredOrder, ...restParams } = params;
const requestParams = {
...restParams,
order: { [cursorIdKey]: "ASC" },
filter: { ...params["filter"] || {}, [moreIdKey]: 0 },
start: -1
};
warnOnShadowedUppercaseParams("fetchList.make", requestParams, this._logger);
let pages = 0;
const maxPages = resolveMaxPages("fetchList.make", options?.maxPages);
while (true) {
assertNotAborted(options?.signal, "fetchList.make", options.method);
const response = await this._b24.actions.v2.call.make({
method: options.method,
params: requestParams,
requestId: options.requestId
});
if (!response.isSuccess) {
this._logger.error("fetchList.make", {
method: options.method,
requestId: options.requestId,
messages: response.getErrorMessages()
}).catch(() => {
});
throw new SdkError({
code: "JSSDK_CORE_B24_FETCH_LIST_METHOD_API_V2",
description: `API Error: ${response.getErrorMessages().join("; ")}`,
status: 500
});
}
const responseData = response.getData();
if (!responseData) {
break;
}
const resultData = null === customKeyForResult ? responseData.result : responseData.result[customKeyForResult];
if (resultData.length === 0) {
break;
}
pages += 1;
yield resultData;
assertNotAborted(options?.signal, "fetchList.make", options.method);
const isShortPage = resultData.length < batchSize;
const lastItem = resultData[resultData.length - 1];
const cursorValue = lastItem ? Number.parseInt(lastItem[idKey], 10) : Number.NaN;
if (Number.isFinite(cursorValue)) {
if (cursorValue === requestParams.filter[moreIdKey]) {
throw cursorStalledError("fetchList.make", CURSOR_STALLED_HINT_LIST);
}
if (!cursorProgressed(cursorValue, requestParams.filter[moreIdKey], "ASC")) {
throw cursorWentBackwardsError("fetchList.make", CURSOR_STALLED_HINT_LIST);
}
if (isShortPage) {
break;
}
requestParams.filter[moreIdKey] = cursorValue;
if (pages >= maxPages) {
throw maxPagesExceededError("fetchList.make", options.method, maxPages);
}
} else {
if (!isShortPage) {
this._logger.warning(`fetchList.make: pagination stops here \u2014 no numeric id could be read from the returned items via idKey "${idKey}". Make sure idKey matches the id field in the response; if the sortable field name differs from it, also set cursorIdKey (e.g. idKey: 'id', cursorIdKey: 'ID').`).catch(() => {
});
}
break;
}
}
}
}
export { FetchListV2 };
//# sourceMappingURL=fetch-list.mjs.map