UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

169 lines (166 loc) • 7.54 kB
/** * @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