@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
329 lines (325 loc) • 13 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/
*/
'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