UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

329 lines (325 loc) • 13 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/ */ 'use strict'; const parseErrorPayload = require('./parse-error-payload.cjs'); const type = require('../../tools/type.cjs'); const text = require('../../tools/text.cjs'); const result = require('../result.cjs'); const ajaxError = require('./ajax-error.cjs'); const b24 = require('../../types/b24.cjs'); const sdkError = require('../sdk-error.cjs'); var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); class AjaxResult extends result.Result { static { __name(this, "AjaxResult"); } _status; _query; _idempotency; _data; constructor(options) { super(); this._data = options.answer ? Object.freeze(options.answer) : void 0; this._query = Object.freeze(structuredClone(options.query)); this._status = options.status; this._idempotency = options.idempotency ? Object.freeze({ ...options.idempotency }) : void 0; if (options.error) { this.addError(options.error, "base-error"); } else { this.#processErrors(); } } get isSuccess() { return this.#getIsSuccess(); } /** * @todo test this predicate */ #getIsSuccess() { return this._errors.size === 0; } /** * The success payload as `{ result, time }` — those two keys, and no others. * * The `restApi:v2` envelope fields `next` and `total` are **not** carried * through. Neither branch below copies them: both rebuild the payload from two * named keys, so `getData()!.next` is `undefined` on a response that really * did carry a next offset. Read them through the methods that exist for it — * {@link AjaxResult.isMore} with {@link AjaxResult.getNext} for paging, * {@link AjaxResult.getTotal} for the row count — or let * `actions.v{2,3}.{callList,fetchList}` page for you. Under `restApi:v3` the * portal sends neither field at all. * * Worth stating here because the Bitrix24 REST reference shows `next` in its * response examples, so expecting it in the payload is the natural reading, * and a hand-rolled loop that reads it stops after one page while looking like * it worked (#482). * * A response whose body is **not an envelope** — anything but a plain object * with a `result` key — is wrapped: the whole body becomes `result`. Without * that the body is simply lost, because this method rebuilds the payload from * two named keys, so everything else on it is projected away and the caller * gets `{ result: undefined, time: undefined }` from a call that succeeded. * * That is not hypothetical. `rest.documentation.openapi` answers with the * OpenAPI document at the top level — no envelope, no `time` — on every portal * measured (an on-premise build, a cloud portal, a cloud sandbox). It is the * method our own docs call the source of truth for v3 discovery, and its * documented snippet reads `getData()?.result`; the wrap is what makes that * snippet true rather than a promise the transport quietly breaks. * * Done here rather than in the constructor on purpose: `#processErrors()` runs * against the raw body and detects an error by its `error` key. Wrapping * earlier would hide `error` one level down and turn every error response into * a "successful" one. By the time this method runs, `isSuccess` has already * been decided. * * `b24phpsdk` resolves the same problem the same way — `Response.php` moves a * body with no `result` key under `result`, naming this very endpoint in its * comment. */ getData() { if (!this.isSuccess) { return void 0; } const payload = this._data; const isEnvelope = payload !== null && typeof payload === "object" && "result" in payload; if (!isEnvelope) { return Object.freeze({ result: payload, // Read off the body when it happens to carry one; never invented. time: payload?.time }); } return Object.freeze({ result: payload.result, time: payload.time }); } /** * If the response contains error data, we'll restore it to an error. * * The parsing lives in {@link parseErrorPayload}, shared with * `AbstractHttp._convertAxiosErrorToAjaxError()`. It used to be written out * here as well, and the two copies had drifted into agreeing on everything * except that neither read `validation[].field` (#423). */ #processErrors() { const parsed = parseErrorPayload.parseErrorPayload(this._data, "JSSDK_RESPONSE_ERROR", "Some error in response"); if (parsed === void 0) { return; } this.addError(this.#createAjaxError({ code: parsed.code, description: parsed.description, status: this._status, validation: parsed.validation, isV3Envelope: parsed.isV3Envelope }), "base-error"); } #createAjaxError(errorData) { return new ajaxError.AjaxError({ code: errorData.code, description: errorData.description, status: errorData.status, validation: errorData.validation, isV3Envelope: errorData.isV3Envelope, requestInfo: { method: this._query.method, params: this._query.params, requestId: this._query.requestId } }); } /** * Alias for {@link AjaxResult.isMore}. * * `restApi:v2` only — see {@link AjaxResult.isMore} for what this returns on * a `restApi:v3` response. */ hasMore() { return this.isMore(); } /** * Whether the `restApi:v2` envelope carries a `next` offset — i.e. the portal * has more rows for this query. * * **`restApi:v2` only.** `restApi:v3` returns no `next` field, so this returns * `false` on a v3 response — which is not the same statement as "there are no * more rows". Do not branch on it for v3; there is nothing to read. * * This is a reader for a protocol field, not a deprecated API: it stays for as * long as `restApi:v2` does, and so does its counterpart * {@link AjaxResult.getNext} — the two together are the manual `restApi:v2` * paging loop, and neither is going away. For new code prefer the list * helpers, which hide the offset bookkeeping and work under both protocol * versions: * - `restApi:v2`: `b24.actions.v2.callList.make` or `b24.actions.v2.fetchList.make` * - `restApi:v3`: `b24.actions.v3.callList.make` or `b24.actions.v3.fetchList.make` */ isMore() { if (!this.isSuccess) { return false; } const payload = this._data; const nextValue = "next" in payload ? payload.next : void 0; return type.Type.isNumber(nextValue); } /** * The row count the `restApi:v2` envelope reports in its `total` field. * * **`restApi:v2` only.** `restApi:v3` returns no `total`, so this returns `0` * on a v3 response — which is not the same statement as "no rows matched". * Do not read it for v3; use * `b24.actions.v3.aggregate.make` with `count` / `countDistinct` instead, * bearing in mind that action is `@experimental` — its contract is measured * now, but no shipped module publishes an `*.aggregate` method, so there may * be nothing to count with. * * This is a reader for a protocol field, not a deprecated API. It is the only * way to obtain a count under `restApi:v2` — the list helpers iterate without * exposing `total`, {@link SuccessPayload} deliberately omits it, and the * `aggregate` action exists for `restApi:v3` only. It therefore stays for as * long as `restApi:v2` does, and is not part of the `3.0.0` removal set. * * That is a decision with a trigger, not an open-ended promise. Revisit it * when either holds: a shipped module publishes an `*.aggregate` method and * `b24.actions.v3.aggregate` loses its `@experimental` tag (a v3 count then * exists in practice, and `getTotal()` has a replacement for the first time), * or Bitrix24 announces a `restApi:v2` sunset date (the field it reads goes * away regardless). Until one of those happens there is nothing to migrate * callers to, which is the whole reason it is still here. * * Note this trigger is specific to the readers. {@link AjaxResult.getNext} and * {@link AjaxResult.fetchNext} already have a working replacement, so nothing * about `aggregate` maturing changes anything for them — a `restApi:v2` sunset * is their only exit condition. */ getTotal() { if (!this.isSuccess) { return 0; } const payload = this._data; const totalValue = "total" in payload ? payload.total : void 0; return text.Text.toInteger(totalValue); } getStatus() { return this._status; } /** * Whether the portal replayed a stored response instead of executing the * method again. * * Only ever `true` for a `restApi:v3` call made with an `idempotencyKey` * that the portal has already seen with the same body: it answers HTTP 200 * with the stored body — the same ids, the same everything — and marks it * with `Idempotent-Replayed: true`. That header is the only difference; a * replay is otherwise indistinguishable from a fresh write. * * `false` for every other response, including a first call that *did* carry * a key. * * @example * declare const orderId: number * // The key names the operation, so a retry of it repeats the same string. * const response = await b24.actions.v3.call.make({ * method: 'tasks.task.add', * params: { fields: { title: 'Ship it', creatorId: 1, responsibleId: 1 } }, * idempotencyKey: `ship-task-for-order-${orderId}` * }) * if (response.isIdempotentReplay()) { * console.log('already created earlier, nothing new was written') * } * * @see https://apidocs.bitrix24.ru/api-reference/rest-v3.html — section * «Повторный вызов без дублей» */ isIdempotentReplay() { return this._idempotency?.replayed ?? false; } /** * The idempotency key the portal echoed back, when it echoed one. * * Useful for correlating a call with its key in a log; the caller already * knows the key it sent, so this is a confirmation rather than a discovery. */ getIdempotencyKey() { return this._idempotency?.key; } getQuery() { return this._query; } /** * Alias for {@link AjaxResult.getNext}, returning `null` where that returns * `false`. * * **`restApi:v2` only** — see {@link AjaxResult.getNext}, including the throw * on a `restApi:v3` client, which this inherits. */ async fetchNext(http) { const data = await this.getNext(http); if (data === false) { return null; } return data; } /** * Re-runs this result's own query with `params.start` set to the `next` offset * the `restApi:v2` envelope reported, and resolves to the following page. * Returns `false` when this result is unsuccessful or has no `next`. * * `restApi:v2` only, and permanently so. Unlike the readers above, this one * acts on the envelope, and `restApi:v3` has no `next` to act on — so it * throws rather than silently returning `false`, which would be * indistinguishable from "last page". That throw is not a transitional * measure; it is the honest answer for a protocol that does not have this * operation. * * For new code prefer `b24.actions.v{2,3}.callList.make` (collect everything) * or `b24.actions.v{2,3}.fetchList.make` (async generator, one page per * iteration — the same page-by-page control this gives, without the manual * offset bookkeeping, and it works under both protocol versions). This method * is kept because it works under `restApi:v2` and deleting it would break * running code for no gain, not because it is the better tool. * * @throws {SdkError} `JSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3` when called against a `restApi:v3` HTTP client. */ async getNext(http) { if (http.apiVersion === b24.ApiVersion.v3) { throw new sdkError.SdkError({ code: "JSSDK_CORE_METHOD_NOT_SUPPORT_IN_API_V3", description: `restApi:v3 not support method getNext`, status: 500 }); } if (!this.isSuccess || !this.isMore()) { return false; } const nextPageQuery = this.#buildNextPageQuery(); return http.call( nextPageQuery.method, nextPageQuery.params ); } #buildNextPageQuery() { const payload = this._data; const nextValue = "next" in payload ? payload.next : void 0; return { ...this._query, params: { ...this._query.params, start: text.Text.toInteger(nextValue) } }; } // Immutable API setData() { throw new ReferenceError("AjaxResult does not allow data modification"); } } exports.AjaxResult = AjaxResult; //# sourceMappingURL=ajax-result.cjs.map