@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
139 lines (136 loc) • 6.52 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 { AbstractAction } from '../abstract-action.mjs';
import { SdkError } from '../../sdk-error.mjs';
import { assertArrayFilter, keysetPaginate, KeysetPaginationError } from './_keyset-paginate.mjs';
import { CURSOR_STALLED_HINT_LIST } from '../_cursor-stalled.mjs';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
class FetchListV3 extends AbstractAction {
static {
__name(this, "FetchListV3");
}
/**
* 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 v3 page](https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/fetch-list-rest-api-ver3/),
* and each one on {@link ActionFetchListV3}.** 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
* `JSSDK_ACTION_CURSOR_STALLED`
* ({@link CURSOR_STALLED_HINT_LIST} names the usual causes). 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.
* - End of data is decided against the largest page seen, never against
* `limit`, which methods are free to cap below the ask. The rule lives in
* {@link keysetPaginate}, which this delegates to.
* - `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 {ActionFetchListV3} 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 { Text } from '@bitrix24/b24jssdk'
*
* interface MainEventLogItem { id: number, userId: number }
* const sixMonthAgo = new Date()
* sixMonthAgo.setMonth((new Date()).getMonth() - 6)
* sixMonthAgo.setHours(0, 0, 0)
* const generator = b24.actions.v3.fetchList.make<MainEventLogItem>({
* method: 'main.eventlog.list',
* params: {
* filter: [
* ['timestampX', '>=', Text.toB24Format(sixMonthAgo)] // created at least 6 months ago
* ],
* select: ['id', 'userId']
* },
* idKey: 'id',
* customKeyForResult: 'items',
* requestId: 'eventlog-123',
* limit: 60
* })
*
* for await (const chunk of generator) {
* // Process chunk (e.g., save to database, analyze, etc.)
* console.log(`Processing ${chunk.length} items`)
* }
*/
async *make(options) {
const batchSize = options?.limit ?? 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(() => {
});
}
assertArrayFilter(params["filter"], "fetchList.make");
const { order: _ignoredOrder, ...restParams } = params;
const requestParams = {
...restParams,
order: { [cursorIdKey]: "ASC" },
filter: [...params["filter"] ?? []],
pagination: { page: 0, limit: batchSize }
};
try {
yield* keysetPaginate(this._b24, this._logger, {
method: options.method,
requestId: options.requestId,
customKeyForResult,
initialCursor: 0,
// Emulated keyset: append the `[cursorIdKey, '>', cursor]` page filter.
buildParams: /* @__PURE__ */ __name((cursor) => ({ ...requestParams, filter: [...requestParams.filter, [cursorIdKey, ">", cursor]] }), "buildParams"),
// Advance by the numeric id read from the last item via `idKey`. A
// non-numeric value (almost always an `idKey` that doesn't match the
// response field — e.g. sorting by `ID` while the response carries a
// lowercase `id`) stops the walk instead of silently truncating.
readNextCursor: /* @__PURE__ */ __name((lastItem) => {
const value = Number.parseInt(lastItem[idKey], 10);
return Number.isFinite(value) ? value : null;
}, "readNextCursor"),
noCursorWarning: `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').`,
errorLabel: "fetchList.make",
actionLabel: "fetchList.make",
stalledCursorHint: CURSOR_STALLED_HINT_LIST,
// Always ascending: the page condition is `[cursorIdKey, '>', cursor]`
// and the request sorts by the same field, so the cursor read off each
// page is strictly greater than the one it was requested with.
cursorDirection: "ASC",
maxPages: options?.maxPages,
signal: options?.signal
});
} catch (error) {
if (error instanceof KeysetPaginationError) {
throw new SdkError({
code: "JSSDK_CORE_B24_FETCH_LIST_METHOD_API_V3",
description: `API Error: ${error.messages.join("; ")}`,
status: 500
});
}
throw error;
}
}
}
export { FetchListV3 };
//# sourceMappingURL=fetch-list.mjs.map