bx-rest 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.en.md +235 -2
- package/README.md +235 -2
- package/esm2022/functions/errors.mjs +18 -2
- package/esm2022/public-api.mjs +6 -2
- package/esm2022/rest/base.mjs +3 -3
- package/esm2022/services/frame/frame.mjs +347 -0
- package/esm2022/services/frame/transport.mjs +224 -0
- package/esm2022/services/frame/types.mjs +2 -0
- package/esm2022/services/http/HttpBX.mjs +39 -17
- package/esm2022/services/http/authRefresh.mjs +89 -0
- package/esm2022/services/http/sessionKey.mjs +8 -1
- package/esm2022/services/navvy/extends/navvy-pag-base.mjs +25 -4
- package/esm2022/services/navvy/navvy-alternative-pagination-navigation.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag-nav-result-key-object.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag-nav-result-key.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag-nav-tasks.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag-nav-with-useless-key.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag.mjs +2 -2
- package/esm2022/typification/settings.mjs +1 -1
- package/fesm2022/bx-rest.mjs +820 -101
- package/fesm2022/bx-rest.mjs.map +1 -1
- package/functions/errors.d.ts +17 -0
- package/package.json +1 -1
- package/public-api.d.ts +5 -1
- package/rest/base.d.ts +1 -1
- package/services/frame/frame.d.ts +191 -0
- package/services/frame/transport.d.ts +75 -0
- package/services/frame/types.d.ts +123 -0
- package/services/http/HttpBX.d.ts +15 -4
- package/services/http/authRefresh.d.ts +19 -0
- package/services/http/sessionKey.d.ts +5 -0
- package/services/navvy/extends/navvy-pag-base.d.ts +11 -0
- package/typification/settings.d.ts +24 -0
package/fesm2022/bx-rest.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { switchMap,
|
|
2
|
-
import { Subscription, of, throwError, from,
|
|
1
|
+
import { switchMap, catchError, map, take, mergeMap as mergeMap$1 } from 'rxjs/operators';
|
|
2
|
+
import { Subscription, of, throwError, from, defer, forkJoin, last, shareReplay, mergeMap, concatMap, toArray, map as map$1, tap } from 'rxjs';
|
|
3
3
|
import clone from 'just-clone';
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import flatten from 'just-flatten-it';
|
|
@@ -587,6 +587,13 @@ class SessionKeyServices {
|
|
|
587
587
|
}
|
|
588
588
|
return this.getKeyAuth() === 'OAuth2';
|
|
589
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Статичные заголовки из `BXRestSettings.headers` — копия, чтобы правка
|
|
592
|
+
* результата не меняла сами настройки.
|
|
593
|
+
*/
|
|
594
|
+
getHeaders() {
|
|
595
|
+
return { ...this.BX_REST_SETTINGS.headers };
|
|
596
|
+
}
|
|
590
597
|
getSessid() {
|
|
591
598
|
const check = this.getSearchParam('sessid');
|
|
592
599
|
if (check) {
|
|
@@ -923,6 +930,178 @@ function serializeBitrixParams(params) {
|
|
|
923
930
|
return stringify(params, BITRIX_STRINGIFY_OPTIONS);
|
|
924
931
|
}
|
|
925
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Why `BXRestSettings.auth.refresh` failed. When a request fails with an
|
|
935
|
+
* authorization error and the refresh fails too, the library rethrows the
|
|
936
|
+
* original request error and puts the refresh failure into its
|
|
937
|
+
* `authRefreshError` field — on the `AxiosError` of `.resVanilla()` and the
|
|
938
|
+
* like, and on the `BXRestApiError` / `Error` of `.res()`.
|
|
939
|
+
*
|
|
940
|
+
* Returns `undefined` when there was no refresh or it succeeded.
|
|
941
|
+
*/
|
|
942
|
+
function getAuthRefreshError(error) {
|
|
943
|
+
return (error && typeof error === 'object')
|
|
944
|
+
? error.authRefreshError
|
|
945
|
+
: undefined;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* A Bitrix-level API error, e.g. `INSUFFICIENT_SCOPE` or `expired_token`.
|
|
949
|
+
*
|
|
950
|
+
* `.res()` fails with it whenever the portal answers with a Bitrix error and
|
|
951
|
+
* a non-2xx HTTP status — which is how Bitrix sends most of its errors. When
|
|
952
|
+
* the error comes in the body of a 2xx response, `.res()` resolves as
|
|
953
|
+
* `undefined` by default and fails with it only with
|
|
954
|
+
* `{ throwOnApiError: true }` — see the "Error handling" section of the README.
|
|
955
|
+
*/
|
|
956
|
+
class BXRestApiError extends Error {
|
|
957
|
+
constructor(error, error_description, details) {
|
|
958
|
+
super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
|
|
959
|
+
this.error = error;
|
|
960
|
+
this.error_description = error_description;
|
|
961
|
+
this.name = 'BXRestApiError';
|
|
962
|
+
this.status = details?.status;
|
|
963
|
+
this.method = details?.method;
|
|
964
|
+
this.authRefreshError = details?.authRefreshError;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
|
|
969
|
+
* REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
|
|
970
|
+
* else — a success payload, an HTML page from a proxy, an empty body.
|
|
971
|
+
*
|
|
972
|
+
* An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
|
|
973
|
+
* missing ID with `{ error: '', error_description: 'Not found' }`.
|
|
974
|
+
*/
|
|
975
|
+
function parseBXRestErrorBody(body) {
|
|
976
|
+
if (!body || typeof body !== 'object' || !('error' in body)) {
|
|
977
|
+
return undefined;
|
|
978
|
+
}
|
|
979
|
+
const { error, error_description } = body;
|
|
980
|
+
if (typeof error === 'string') {
|
|
981
|
+
return {
|
|
982
|
+
error,
|
|
983
|
+
error_description: typeof error_description === 'string' ? error_description : undefined
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
987
|
+
const { code, message } = error;
|
|
988
|
+
return {
|
|
989
|
+
error: String(code),
|
|
990
|
+
error_description: typeof message === 'string' ? message : undefined
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
return undefined;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Turns an HTTP failure into a `BXRestApiError` when the portal answered with
|
|
997
|
+
* a Bitrix error body. Bitrix sends most errors with a non-2xx status
|
|
998
|
+
* (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
|
|
999
|
+
* `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
|
|
1000
|
+
* rejects on those.
|
|
1001
|
+
*
|
|
1002
|
+
* Returns `undefined` for network errors and for responses that are not a
|
|
1003
|
+
* Bitrix error body, so the caller can keep its own handling for them.
|
|
1004
|
+
*/
|
|
1005
|
+
function bxRestApiErrorFromHttp(err, method) {
|
|
1006
|
+
if (!axios.isAxiosError(err) || !err.response) {
|
|
1007
|
+
return undefined;
|
|
1008
|
+
}
|
|
1009
|
+
const parsed = parseBXRestErrorBody(err.response.data);
|
|
1010
|
+
if (!parsed) {
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
return new BXRestApiError(parsed.error, parsed.error_description, {
|
|
1014
|
+
status: err.response.status,
|
|
1015
|
+
method,
|
|
1016
|
+
cause: err,
|
|
1017
|
+
authRefreshError: getAuthRefreshError(err)
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Коды ошибок Bitrix, которые лечатся продлением токена. `expired_token` —
|
|
1023
|
+
* единственный системный код, для которого документация Bitrix24 советует
|
|
1024
|
+
* "обновить токены и повторить вызов". `invalid_token` в списке системных
|
|
1025
|
+
* ошибок документации нет, но так же считает официальный `@bitrix24/b24jssdk`.
|
|
1026
|
+
*
|
|
1027
|
+
* Остальные системные ошибки тоже приходят со статусом 401
|
|
1028
|
+
* (`insufficient_scope`, `NO_AUTH_FOUND`, `ACCESS_DENIED`,
|
|
1029
|
+
* `INVALID_CREDENTIALS`, `user_access_error`, ...), но новый токен их
|
|
1030
|
+
* не исправит — поэтому смотрим на код, а не только на статус.
|
|
1031
|
+
*/
|
|
1032
|
+
const AUTH_ERROR_CODES = ['expired_token', 'invalid_token'];
|
|
1033
|
+
/**
|
|
1034
|
+
* Условие продления по умолчанию (`BXRestSettings.auth.isAuthError`):
|
|
1035
|
+
* - ответ с ошибкой Bitrix `expired_token` или `invalid_token` — при любом статусе;
|
|
1036
|
+
* - ответ 401 без тела ошибки Bitrix — например, пустой ответ своего
|
|
1037
|
+
* бэкенд-прокси, у которого нет действующего токена.
|
|
1038
|
+
*
|
|
1039
|
+
* Ответ 401 с другим кодом Bitrix ошибкой авторизации не считается.
|
|
1040
|
+
*/
|
|
1041
|
+
function isBXRestAuthError(error) {
|
|
1042
|
+
if (!axios.isAxiosError(error) || !error.response) {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
const parsed = parseBXRestErrorBody(error.response.data);
|
|
1046
|
+
if (parsed) {
|
|
1047
|
+
return AUTH_ERROR_CODES.includes(parsed.error);
|
|
1048
|
+
}
|
|
1049
|
+
return error.response.status === 401;
|
|
1050
|
+
}
|
|
1051
|
+
// Общее на всю библиотеку состояние продления — как и сами BXRestSettings.
|
|
1052
|
+
// pendingRefresh — идущее сейчас продление, его ждут все упавшие запросы;
|
|
1053
|
+
// refreshCount — сколько продлений уже завершилось успешно.
|
|
1054
|
+
let pendingRefresh;
|
|
1055
|
+
let refreshCount = 0;
|
|
1056
|
+
function sharedRefresh(refresh) {
|
|
1057
|
+
if (!pendingRefresh) {
|
|
1058
|
+
pendingRefresh = Promise.resolve()
|
|
1059
|
+
// Через then, чтобы синхронное исключение в refresh() тоже стало
|
|
1060
|
+
// отклонённым промисом, а не вылетело из catchError
|
|
1061
|
+
.then(() => refresh())
|
|
1062
|
+
.then(() => {
|
|
1063
|
+
refreshCount++;
|
|
1064
|
+
})
|
|
1065
|
+
.finally(() => {
|
|
1066
|
+
pendingRefresh = undefined;
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return pendingRefresh;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Повтор запроса после продления авторизации (`BXRestSettings.auth.refresh`).
|
|
1073
|
+
*
|
|
1074
|
+
* `request$` должен быть "холодным" и читать настройки при подписке (как
|
|
1075
|
+
* `defer()` в HttpBXServices): повтор — это повторная подписка, поэтому
|
|
1076
|
+
* токен, адрес и заголовки берутся уже новые. На второй попытке ошибка не
|
|
1077
|
+
* перехватывается — запрос повторяется не больше одного раза.
|
|
1078
|
+
*/
|
|
1079
|
+
function withAuthRefresh(request$) {
|
|
1080
|
+
return defer(() => {
|
|
1081
|
+
const { refresh, isAuthError = isBXRestAuthError } = BXRestSettings.value.auth;
|
|
1082
|
+
if (!refresh) {
|
|
1083
|
+
return request$;
|
|
1084
|
+
}
|
|
1085
|
+
const refreshCountAtStart = refreshCount;
|
|
1086
|
+
return request$.pipe(catchError((error) => {
|
|
1087
|
+
if (!isAuthError(error)) {
|
|
1088
|
+
return throwError(() => error);
|
|
1089
|
+
}
|
|
1090
|
+
// Пока этот запрос был в пути, токен уже продлили по ошибке другого
|
|
1091
|
+
// запроса — повторяем без нового продления
|
|
1092
|
+
const refreshed = (refreshCount !== refreshCountAtStart)
|
|
1093
|
+
? Promise.resolve()
|
|
1094
|
+
: sharedRefresh(refresh);
|
|
1095
|
+
return from(refreshed).pipe(catchError((refreshError) => {
|
|
1096
|
+
if (error && typeof error === 'object') {
|
|
1097
|
+
error.authRefreshError = refreshError;
|
|
1098
|
+
}
|
|
1099
|
+
return throwError(() => error);
|
|
1100
|
+
}), switchMap(() => request$));
|
|
1101
|
+
}));
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
926
1105
|
class HttpBXServices extends HttpServices {
|
|
927
1106
|
/**
|
|
928
1107
|
* Не считайте этот метод образцом/финальным API.
|
|
@@ -970,27 +1149,38 @@ class HttpBXServices extends HttpServices {
|
|
|
970
1149
|
*
|
|
971
1150
|
* @param name
|
|
972
1151
|
* @param params
|
|
1152
|
+
* @param restApi
|
|
1153
|
+
* @param headers заголовки только для этого запроса, поверх `BXRestSettings.headers`
|
|
973
1154
|
*/
|
|
974
|
-
post(name, params = {}, restApi = false) {
|
|
1155
|
+
post(name, params = {}, restApi = false, headers) {
|
|
975
1156
|
const method = this.getNameMethod(name);
|
|
976
|
-
return this.httpPost(restApi ? `api/${method}` : method, params);
|
|
1157
|
+
return this.httpPost(restApi ? `api/${method}` : method, params, undefined, headers);
|
|
977
1158
|
}
|
|
978
1159
|
/**
|
|
979
1160
|
* Второстепенный метод для запросов. По возможности лучше не использовать
|
|
980
1161
|
*
|
|
981
1162
|
* @param name
|
|
982
1163
|
* @param params
|
|
1164
|
+
* @param headers заголовки только для этого запроса, поверх `BXRestSettings.headers`
|
|
983
1165
|
*/
|
|
984
|
-
get(name, params = {}) {
|
|
985
|
-
return this.httpGet(this.getNameMethod(name), params);
|
|
1166
|
+
get(name, params = {}, headers) {
|
|
1167
|
+
return this.httpGet(this.getNameMethod(name), params, undefined, headers);
|
|
986
1168
|
}
|
|
987
|
-
|
|
1169
|
+
// settings — только для совместимости сигнатуры с HttpServices: здесь тело
|
|
1170
|
+
// уходит JSON-ом/urlencoded, и пересчёт дат из iHttpParamSettings не нужен.
|
|
1171
|
+
httpPost(url, params = {}, settings, headers) {
|
|
988
1172
|
// defer() откладывает всё содержимое (в том числе чтение BXRestSettings
|
|
989
1173
|
// через this.session.*) до реальной подписки — иначе даже с "живым"
|
|
990
1174
|
// геттером в SessionKeyServices настройки читались бы в момент вызова
|
|
991
1175
|
// .res(), а не в момент subscribe()/firstValueFrom(), что и обещано
|
|
992
1176
|
// в README как поведение "ленивого" REST-вызова.
|
|
993
|
-
|
|
1177
|
+
//
|
|
1178
|
+
// withAuthRefresh() повторяет запрос после BXRestSettings.auth.refresh —
|
|
1179
|
+
// повторной подпиской на этот же defer(), поэтому повтор заново читает
|
|
1180
|
+
// токен, адрес и заголовки. Через httpPost/httpGet идут все запросы
|
|
1181
|
+
// библиотеки (post/get, batch, все виды Navvy-запросов), так что повтор
|
|
1182
|
+
// встроен только здесь.
|
|
1183
|
+
return withAuthRefresh(defer(() => {
|
|
994
1184
|
const paramsClone = clone(params);
|
|
995
1185
|
const key = this.session.getKeyAuth();
|
|
996
1186
|
const auth = this.session.getAuthParams();
|
|
@@ -1012,24 +1202,23 @@ class HttpBXServices extends HttpServices {
|
|
|
1012
1202
|
// Преобразуем параметры в формат x-www-form-urlencoded
|
|
1013
1203
|
const bodyString = this.serializeBitrixParams(paramsClone);
|
|
1014
1204
|
return from(this.axiosInstance.post(fullUrl, bodyString, {
|
|
1015
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1205
|
+
headers: this.buildHeaders(headers, { 'Content-Type': 'application/x-www-form-urlencoded' }),
|
|
1016
1206
|
}).then((response) => response.data));
|
|
1017
1207
|
}
|
|
1018
1208
|
return from(this.axiosInstance.post(fullUrl, paramsClone, {
|
|
1019
1209
|
withCredentials: this.session.getWithCredentials(),
|
|
1020
|
-
headers: {
|
|
1021
|
-
'Content-Type': 'application/json',
|
|
1022
|
-
}
|
|
1210
|
+
headers: this.buildHeaders(headers, { 'Content-Type': 'application/json' }),
|
|
1023
1211
|
}).then((response) => response.data));
|
|
1024
1212
|
}));
|
|
1025
|
-
});
|
|
1213
|
+
}));
|
|
1026
1214
|
}
|
|
1027
|
-
httpGet(url, params = {}) {
|
|
1215
|
+
httpGet(url, params = {}, settings, headers) {
|
|
1028
1216
|
// Та же причина, что и у httpPost выше: откладываем чтение BXRestSettings
|
|
1029
1217
|
// до подписки, чтобы `get()` вёл себя так же "лениво", как и `post()`
|
|
1030
1218
|
// (симметрия, которой не было до этого — GET резолвил настройки в момент
|
|
1031
|
-
// вызова, а не в момент subscribe()).
|
|
1032
|
-
|
|
1219
|
+
// вызова, а не в момент subscribe()). Повтор после продления
|
|
1220
|
+
// авторизации — как в httpPost.
|
|
1221
|
+
return withAuthRefresh(defer(() => {
|
|
1033
1222
|
const paramsClone = clone(params);
|
|
1034
1223
|
const auth = this.session.getAuthParams();
|
|
1035
1224
|
const checkIsOn = this.session.getCheckAuthParamsIsOn();
|
|
@@ -1046,9 +1235,20 @@ class HttpBXServices extends HttpServices {
|
|
|
1046
1235
|
const fullUrl = prepareBaseAddress(baseUrl) + url;
|
|
1047
1236
|
const queryString = new URLSearchParams(paramsClone).toString();
|
|
1048
1237
|
const finalUrl = `${fullUrl}?${queryString}`;
|
|
1049
|
-
return from(this.axiosInstance.get(finalUrl
|
|
1238
|
+
return from(this.axiosInstance.get(finalUrl, {
|
|
1239
|
+
headers: this.buildHeaders(headers),
|
|
1240
|
+
}).then((response) => response.data));
|
|
1050
1241
|
}));
|
|
1051
|
-
});
|
|
1242
|
+
}));
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Заголовки запроса: `BXRestSettings.headers`, поверх них — заголовки
|
|
1246
|
+
* конкретного запроса, поверх всего — `required`. В `required` идёт
|
|
1247
|
+
* `Content-Type`: от него зависит, как сервер разберёт тело, поэтому
|
|
1248
|
+
* пользовательские заголовки его не перебивают.
|
|
1249
|
+
*/
|
|
1250
|
+
buildHeaders(headers, required = {}) {
|
|
1251
|
+
return { ...this.session.getHeaders(), ...headers, ...required };
|
|
1052
1252
|
}
|
|
1053
1253
|
serializeBitrixParams(params) {
|
|
1054
1254
|
return serializeBitrixParams(params);
|
|
@@ -1058,78 +1258,6 @@ class HttpBXServices extends HttpServices {
|
|
|
1058
1258
|
}
|
|
1059
1259
|
}
|
|
1060
1260
|
|
|
1061
|
-
/**
|
|
1062
|
-
* A Bitrix-level API error, e.g. `INSUFFICIENT_SCOPE` or `expired_token`.
|
|
1063
|
-
*
|
|
1064
|
-
* `.res()` fails with it whenever the portal answers with a Bitrix error and
|
|
1065
|
-
* a non-2xx HTTP status — which is how Bitrix sends most of its errors. When
|
|
1066
|
-
* the error comes in the body of a 2xx response, `.res()` resolves as
|
|
1067
|
-
* `undefined` by default and fails with it only with
|
|
1068
|
-
* `{ throwOnApiError: true }` — see the "Error handling" section of the README.
|
|
1069
|
-
*/
|
|
1070
|
-
class BXRestApiError extends Error {
|
|
1071
|
-
constructor(error, error_description, details) {
|
|
1072
|
-
super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
|
|
1073
|
-
this.error = error;
|
|
1074
|
-
this.error_description = error_description;
|
|
1075
|
-
this.name = 'BXRestApiError';
|
|
1076
|
-
this.status = details?.status;
|
|
1077
|
-
this.method = details?.method;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
|
|
1082
|
-
* REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
|
|
1083
|
-
* else — a success payload, an HTML page from a proxy, an empty body.
|
|
1084
|
-
*
|
|
1085
|
-
* An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
|
|
1086
|
-
* missing ID with `{ error: '', error_description: 'Not found' }`.
|
|
1087
|
-
*/
|
|
1088
|
-
function parseBXRestErrorBody(body) {
|
|
1089
|
-
if (!body || typeof body !== 'object' || !('error' in body)) {
|
|
1090
|
-
return undefined;
|
|
1091
|
-
}
|
|
1092
|
-
const { error, error_description } = body;
|
|
1093
|
-
if (typeof error === 'string') {
|
|
1094
|
-
return {
|
|
1095
|
-
error,
|
|
1096
|
-
error_description: typeof error_description === 'string' ? error_description : undefined
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
if (error && typeof error === 'object' && 'code' in error) {
|
|
1100
|
-
const { code, message } = error;
|
|
1101
|
-
return {
|
|
1102
|
-
error: String(code),
|
|
1103
|
-
error_description: typeof message === 'string' ? message : undefined
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
return undefined;
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Turns an HTTP failure into a `BXRestApiError` when the portal answered with
|
|
1110
|
-
* a Bitrix error body. Bitrix sends most errors with a non-2xx status
|
|
1111
|
-
* (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
|
|
1112
|
-
* `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
|
|
1113
|
-
* rejects on those.
|
|
1114
|
-
*
|
|
1115
|
-
* Returns `undefined` for network errors and for responses that are not a
|
|
1116
|
-
* Bitrix error body, so the caller can keep its own handling for them.
|
|
1117
|
-
*/
|
|
1118
|
-
function bxRestApiErrorFromHttp(err, method) {
|
|
1119
|
-
if (!axios.isAxiosError(err) || !err.response) {
|
|
1120
|
-
return undefined;
|
|
1121
|
-
}
|
|
1122
|
-
const parsed = parseBXRestErrorBody(err.response.data);
|
|
1123
|
-
if (!parsed) {
|
|
1124
|
-
return undefined;
|
|
1125
|
-
}
|
|
1126
|
-
return new BXRestApiError(parsed.error, parsed.error_description, {
|
|
1127
|
-
status: err.response.status,
|
|
1128
|
-
method,
|
|
1129
|
-
cause: err
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1261
|
class NavvyPagBase {
|
|
1134
1262
|
constructor(url, param, map = undefined, restApi = false) {
|
|
1135
1263
|
this.url = url;
|
|
@@ -1139,8 +1267,21 @@ class NavvyPagBase {
|
|
|
1139
1267
|
this.pageSize = 50; // стандартный размер страницы
|
|
1140
1268
|
this.http = new HttpBXServices();
|
|
1141
1269
|
}
|
|
1270
|
+
/**
|
|
1271
|
+
* HTTP-заголовки только для этого запроса — поверх `BXRestSettings.headers`
|
|
1272
|
+
* (при совпадении имени побеждает значение отсюда). Повторные вызовы
|
|
1273
|
+
* дополняют уже заданные заголовки. Действует на `.res()`, `.resVanilla()`,
|
|
1274
|
+
* `.mapForVanilla()` и на все batch-запросы `.resAll()`.
|
|
1275
|
+
*
|
|
1276
|
+
* @example
|
|
1277
|
+
* bx.crm.deal.list({ ... }).withHeaders({ 'X-Request-Source': 'report' }).resAll()
|
|
1278
|
+
*/
|
|
1279
|
+
withHeaders(headers) {
|
|
1280
|
+
this.requestHeaders = { ...this.requestHeaders, ...headers };
|
|
1281
|
+
return this;
|
|
1282
|
+
}
|
|
1142
1283
|
resVanilla() {
|
|
1143
|
-
return this.http.post(this.url, this.param, this.restApi);
|
|
1284
|
+
return this.http.post(this.url, this.param, this.restApi, this.requestHeaders);
|
|
1144
1285
|
}
|
|
1145
1286
|
mapForVanilla() {
|
|
1146
1287
|
return this.resVanilla()
|
|
@@ -1178,7 +1319,15 @@ class NavvyPagBase {
|
|
|
1178
1319
|
if (apiError) {
|
|
1179
1320
|
throw apiError;
|
|
1180
1321
|
}
|
|
1181
|
-
|
|
1322
|
+
// Сообщение прежнее, но исходная ошибка остаётся в cause — без неё
|
|
1323
|
+
// терялся HTTP-статус ответа не от Bitrix (например, пустого 401 от
|
|
1324
|
+
// бэкенд-прокси): он был только в тексте сообщения.
|
|
1325
|
+
const error = new Error((err.error) ? err.error : err, { cause: err });
|
|
1326
|
+
const authRefreshError = getAuthRefreshError(err);
|
|
1327
|
+
if (authRefreshError !== undefined) {
|
|
1328
|
+
Object.assign(error, { authRefreshError });
|
|
1329
|
+
}
|
|
1330
|
+
throw error;
|
|
1182
1331
|
}));
|
|
1183
1332
|
}
|
|
1184
1333
|
}
|
|
@@ -3106,8 +3255,8 @@ class BXRest {
|
|
|
3106
3255
|
profile() {
|
|
3107
3256
|
return this.http.post(['profile']);
|
|
3108
3257
|
}
|
|
3109
|
-
batch(param) {
|
|
3110
|
-
return this.http.post(['batch'], param);
|
|
3258
|
+
batch(param, headers) {
|
|
3259
|
+
return this.http.post(['batch'], param, false, headers);
|
|
3111
3260
|
}
|
|
3112
3261
|
}
|
|
3113
3262
|
|
|
@@ -3159,7 +3308,7 @@ class NavvyAlterPagNav extends NavvyPagBase {
|
|
|
3159
3308
|
return this.http.getNameMethod(this.url)
|
|
3160
3309
|
+ '?' + serializeBitrixParams(param);
|
|
3161
3310
|
})
|
|
3162
|
-
});
|
|
3311
|
+
}, this.requestHeaders);
|
|
3163
3312
|
});
|
|
3164
3313
|
return from(batchRequests).pipe(concatMap(request => request), // Выполняет запросы по очереди
|
|
3165
3314
|
toArray(), // Собирает все результаты в массив
|
|
@@ -3215,7 +3364,7 @@ class NavvyPagNavWithUselessKey extends NavvyPagBase {
|
|
|
3215
3364
|
return this.http.getNameMethod(this.url)
|
|
3216
3365
|
+ '?' + serializeBitrixParams(param);
|
|
3217
3366
|
})
|
|
3218
|
-
}).pipe(map(v => {
|
|
3367
|
+
}, this.requestHeaders).pipe(map(v => {
|
|
3219
3368
|
if (v && instanceOfiBXRestAnswerSuccess(v) && v.result && v.result.result && items.result) {
|
|
3220
3369
|
return Object.assign({}, items.result, v.result.result);
|
|
3221
3370
|
}
|
|
@@ -3301,7 +3450,7 @@ class NavvyPagNavTasks extends NavvyPagBase {
|
|
|
3301
3450
|
return this.http.getNameMethod(this.url)
|
|
3302
3451
|
+ '?' + serializeBitrixParams(param);
|
|
3303
3452
|
})
|
|
3304
|
-
});
|
|
3453
|
+
}, this.requestHeaders);
|
|
3305
3454
|
});
|
|
3306
3455
|
return from(batchRequests).pipe(concatMap(request => request), // Выполняет запросы по очереди
|
|
3307
3456
|
toArray(), // Собирает все результаты в массив
|
|
@@ -3365,7 +3514,7 @@ class NavvyPag extends NavvyPagBase {
|
|
|
3365
3514
|
return this.http.getNameMethod(this.url)
|
|
3366
3515
|
+ '?' + serializeBitrixParams(param);
|
|
3367
3516
|
})
|
|
3368
|
-
}).pipe();
|
|
3517
|
+
}, this.requestHeaders).pipe();
|
|
3369
3518
|
});
|
|
3370
3519
|
return from(batchRequests).pipe(concatMap(request => request), // Выполняет запросы по очереди
|
|
3371
3520
|
toArray(), // Собирает все результаты в массив
|
|
@@ -3435,7 +3584,7 @@ class NavvyPagNavResultKey extends NavvyPagBase {
|
|
|
3435
3584
|
return this.http.getNameMethod(this.url)
|
|
3436
3585
|
+ '?' + serializeBitrixParams(param);
|
|
3437
3586
|
})
|
|
3438
|
-
});
|
|
3587
|
+
}, this.requestHeaders);
|
|
3439
3588
|
});
|
|
3440
3589
|
return from(batchRequests).pipe(concatMap(request => request), toArray(), map(results => {
|
|
3441
3590
|
const firstPage = items.result?.[this.key];
|
|
@@ -3491,7 +3640,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
|
|
|
3491
3640
|
return bxRest.batch({
|
|
3492
3641
|
halt: 1,
|
|
3493
3642
|
cmd: [this.http.getNameMethod(this.url) + '?' + serializeBitrixParams(param)]
|
|
3494
|
-
});
|
|
3643
|
+
}, this.requestHeaders);
|
|
3495
3644
|
});
|
|
3496
3645
|
return from(commands).pipe(concatMap(request => request), toArray(), mergeMap(results => {
|
|
3497
3646
|
const merged = { ...(items.result?.[this.key] ?? {}) };
|
|
@@ -20197,6 +20346,576 @@ class BXRestMap {
|
|
|
20197
20346
|
}
|
|
20198
20347
|
}
|
|
20199
20348
|
|
|
20349
|
+
/**
|
|
20350
|
+
* Обмен сообщениями между iframe приложения и порталом Bitrix24 — тот
|
|
20351
|
+
* транспорт, на котором построены команды BX24.js: страница приложения шлёт
|
|
20352
|
+
* команду родительскому окну портала через postMessage, портал отвечает
|
|
20353
|
+
* сообщением с тем же callbackId.
|
|
20354
|
+
*
|
|
20355
|
+
* ВНИМАНИЕ: протокол внутренний и недокументированный — Bitrix24 может
|
|
20356
|
+
* изменить его без предупреждения. Транспорт повторяет BX24.js
|
|
20357
|
+
* (https://api.bitrix24.com/api/v1/, скачан 2026-10-29, Last-Modified
|
|
20358
|
+
* 2022-03-17, sha256 945ad491df8ffb7490b89f4914a439b5a3f575c950c3480562815716a3e07f23)
|
|
20359
|
+
* и сверен с ответной стороной портала — `/bitrix/js/rest/applayout.js`
|
|
20360
|
+
* (Last-Modified 2024-05-15, sha256 3efd6ab57a4778493a26d10ae0987a82e19fc012ed9935f5c6afa78c4052fcb5).
|
|
20361
|
+
* Всё знание о протоколе — только в этом модуле (`services/frame`).
|
|
20362
|
+
*/
|
|
20363
|
+
/**
|
|
20364
|
+
* Ошибка обращения к порталу из iframe. `code`:
|
|
20365
|
+
* - `NOT_IN_FRAME` — страница открыта не в iframe приложения Bitrix24;
|
|
20366
|
+
* - `TIMEOUT` — портал не ответил за отведённое время;
|
|
20367
|
+
* - `NO_AUTH` — портал ответил без токена;
|
|
20368
|
+
* - `BAD_RESPONSE` — ответ портала не удалось разобрать;
|
|
20369
|
+
* - `CANCELLED` — ожидание отменено через `signal` или новым вызовом того же диалога;
|
|
20370
|
+
* - `ACCESS_DENIED` — команда доступна только администратору портала.
|
|
20371
|
+
*/
|
|
20372
|
+
class BXRestFrameError extends Error {
|
|
20373
|
+
constructor(code, message) {
|
|
20374
|
+
super(message);
|
|
20375
|
+
this.code = code;
|
|
20376
|
+
this.name = 'BXRestFrameError';
|
|
20377
|
+
}
|
|
20378
|
+
}
|
|
20379
|
+
/**
|
|
20380
|
+
* Параметры портала (DOMAIN, PROTOCOL, APP_SID) читаются в конструкторе —
|
|
20381
|
+
* так же, как это делает BX24.js, из `window.name`: портал задаёт имя
|
|
20382
|
+
* iframe приложения в виде `DOMAIN|PROTOCOL|APP_SID`. Если там их нет —
|
|
20383
|
+
* из query страницы (`?DOMAIN=…&PROTOCOL=…&APP_SID=…`), с которым портал
|
|
20384
|
+
* открывает обработчик приложения. Конструктор не бросает исключений:
|
|
20385
|
+
* вне iframe ошибку вернут сами команды.
|
|
20386
|
+
*/
|
|
20387
|
+
class FrameTransport {
|
|
20388
|
+
constructor() {
|
|
20389
|
+
this.frame = readPortalFrame();
|
|
20390
|
+
}
|
|
20391
|
+
/** Домен портала из параметров фрейма — как `BX24.getDomain()`. */
|
|
20392
|
+
get domain() {
|
|
20393
|
+
return this.frame?.domain ?? '';
|
|
20394
|
+
}
|
|
20395
|
+
/**
|
|
20396
|
+
* Команда, на которую портал не отвечает (`reloadWindow`, `imCallTo`, ...).
|
|
20397
|
+
* Вне iframe бросает `BXRestFrameError` с кодом `NOT_IN_FRAME`.
|
|
20398
|
+
*/
|
|
20399
|
+
post(command, params) {
|
|
20400
|
+
this.send(this.requireFrame(), command, params, '');
|
|
20401
|
+
}
|
|
20402
|
+
/** Команда с одним ответом портала. */
|
|
20403
|
+
request(command, params, options) {
|
|
20404
|
+
return new Promise((resolve, reject) => {
|
|
20405
|
+
let frame;
|
|
20406
|
+
try {
|
|
20407
|
+
frame = this.requireFrame();
|
|
20408
|
+
}
|
|
20409
|
+
catch (error) {
|
|
20410
|
+
reject(error);
|
|
20411
|
+
return;
|
|
20412
|
+
}
|
|
20413
|
+
const { timeout, onTimeout = 'reject', signal } = options;
|
|
20414
|
+
if (signal?.aborted) {
|
|
20415
|
+
reject(cancelled(command));
|
|
20416
|
+
return;
|
|
20417
|
+
}
|
|
20418
|
+
const callbackId = uniqId();
|
|
20419
|
+
let timer;
|
|
20420
|
+
const finish = () => {
|
|
20421
|
+
clearTimeout(timer);
|
|
20422
|
+
stopListening();
|
|
20423
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20424
|
+
};
|
|
20425
|
+
const onAbort = () => {
|
|
20426
|
+
finish();
|
|
20427
|
+
reject(cancelled(command));
|
|
20428
|
+
};
|
|
20429
|
+
const stopListening = this.listen(frame, callbackId, payload => {
|
|
20430
|
+
finish();
|
|
20431
|
+
const answer = parsePayload(payload);
|
|
20432
|
+
if (answer.ok) {
|
|
20433
|
+
resolve(answer.value);
|
|
20434
|
+
}
|
|
20435
|
+
else {
|
|
20436
|
+
// В тексте ошибки — только команда, без тела ответа: в нём бывают токены
|
|
20437
|
+
reject(new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not JSON.`));
|
|
20438
|
+
}
|
|
20439
|
+
});
|
|
20440
|
+
if (timeout !== false) {
|
|
20441
|
+
timer = setTimeout(() => {
|
|
20442
|
+
finish();
|
|
20443
|
+
if (onTimeout === 'resolve') {
|
|
20444
|
+
resolve(undefined);
|
|
20445
|
+
}
|
|
20446
|
+
else {
|
|
20447
|
+
reject(new BXRestFrameError('TIMEOUT', [
|
|
20448
|
+
`Bitrix24 did not answer ${command} within ${timeout} ms:`,
|
|
20449
|
+
'the page is not opened inside a Bitrix24 portal iframe, or the portal could not process the command.'
|
|
20450
|
+
].join(' ')));
|
|
20451
|
+
}
|
|
20452
|
+
}, timeout);
|
|
20453
|
+
}
|
|
20454
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
20455
|
+
this.send(frame, command, params, callbackId);
|
|
20456
|
+
});
|
|
20457
|
+
}
|
|
20458
|
+
/**
|
|
20459
|
+
* Команда, на которую портал отвечает многократно (`placementBindEvent`):
|
|
20460
|
+
* `handler` вызывается на каждый ответ. Возвращает функцию отписки —
|
|
20461
|
+
* она только перестаёт слушать: отменить подписку на стороне портала
|
|
20462
|
+
* протокол не позволяет. Вне iframe бросает `BXRestFrameError`.
|
|
20463
|
+
*/
|
|
20464
|
+
subscribe(command, params, handler) {
|
|
20465
|
+
const frame = this.requireFrame();
|
|
20466
|
+
const callbackId = uniqId();
|
|
20467
|
+
const stopListening = this.listen(frame, callbackId, payload => {
|
|
20468
|
+
const answer = parsePayload(payload);
|
|
20469
|
+
if (answer.ok) {
|
|
20470
|
+
handler(answer.value);
|
|
20471
|
+
}
|
|
20472
|
+
});
|
|
20473
|
+
this.send(frame, command, params, callbackId);
|
|
20474
|
+
return stopListening;
|
|
20475
|
+
}
|
|
20476
|
+
requireFrame() {
|
|
20477
|
+
if (!this.frame) {
|
|
20478
|
+
throw new BXRestFrameError('NOT_IN_FRAME', 'Bitrix24 frame parameters (DOMAIN, APP_SID) were not found in window.name or in the URL query: '
|
|
20479
|
+
+ 'the page is not opened as a Bitrix24 application.');
|
|
20480
|
+
}
|
|
20481
|
+
if (window.parent === window) {
|
|
20482
|
+
throw new BXRestFrameError('NOT_IN_FRAME', 'The page is not opened inside an iframe, so there is no Bitrix24 portal to send commands to.');
|
|
20483
|
+
}
|
|
20484
|
+
return this.frame;
|
|
20485
|
+
}
|
|
20486
|
+
/**
|
|
20487
|
+
* Формат — как у BX24.js. Обычно строка `команда:параметры:callbackId:APP_SID`
|
|
20488
|
+
* (портал разбирает её с конца, поэтому параметры и callbackId могут быть
|
|
20489
|
+
* пустыми); для команд с `:` в имени — объект.
|
|
20490
|
+
*/
|
|
20491
|
+
send(frame, command, params, callbackId) {
|
|
20492
|
+
const message = command.includes(':')
|
|
20493
|
+
? { method: command, params: params ? params : '', callback: callbackId, appSid: frame.appSid }
|
|
20494
|
+
: `${command}:${params ? JSON.stringify(params) : ''}:${callbackId}:${frame.appSid}`;
|
|
20495
|
+
window.parent.postMessage(message, frame.origin);
|
|
20496
|
+
}
|
|
20497
|
+
/** Ответ портала — строка `callbackId:JSON` (или `callbackId:`, если ответ пустой). */
|
|
20498
|
+
listen(frame, callbackId, onAnswer) {
|
|
20499
|
+
const portalWindow = window.parent;
|
|
20500
|
+
const onMessage = (event) => {
|
|
20501
|
+
// Ответ принимаем только от родительского окна и только с origin
|
|
20502
|
+
// портала — BX24.js проверяет лишь origin
|
|
20503
|
+
if (event.source !== portalWindow || event.origin !== frame.origin || typeof event.data !== 'string') {
|
|
20504
|
+
return;
|
|
20505
|
+
}
|
|
20506
|
+
const separator = event.data.indexOf(':');
|
|
20507
|
+
const id = separator < 0 ? event.data : event.data.slice(0, separator);
|
|
20508
|
+
if (id === callbackId) {
|
|
20509
|
+
onAnswer(separator < 0 ? '' : event.data.slice(separator + 1));
|
|
20510
|
+
}
|
|
20511
|
+
};
|
|
20512
|
+
window.addEventListener('message', onMessage);
|
|
20513
|
+
return () => window.removeEventListener('message', onMessage);
|
|
20514
|
+
}
|
|
20515
|
+
}
|
|
20516
|
+
function parsePayload(payload) {
|
|
20517
|
+
if (payload === '') {
|
|
20518
|
+
return { ok: true, value: undefined };
|
|
20519
|
+
}
|
|
20520
|
+
try {
|
|
20521
|
+
return { ok: true, value: JSON.parse(payload) };
|
|
20522
|
+
}
|
|
20523
|
+
catch {
|
|
20524
|
+
return { ok: false };
|
|
20525
|
+
}
|
|
20526
|
+
}
|
|
20527
|
+
function cancelled(command) {
|
|
20528
|
+
return new BXRestFrameError('CANCELLED', `Waiting for the Bitrix24 answer to ${command} was cancelled.`);
|
|
20529
|
+
}
|
|
20530
|
+
function readPortalFrame() {
|
|
20531
|
+
if (typeof window === 'undefined') {
|
|
20532
|
+
return undefined;
|
|
20533
|
+
}
|
|
20534
|
+
// BX24.js: window.name = 'DOMAIN|PROTOCOL|APP_SID'
|
|
20535
|
+
const [nameDomain, nameProtocol, nameAppSid] = (typeof window.name === 'string' ? window.name : '').split('|');
|
|
20536
|
+
const fromName = toPortalFrame(nameDomain, nameProtocol, nameAppSid);
|
|
20537
|
+
if (fromName) {
|
|
20538
|
+
return fromName;
|
|
20539
|
+
}
|
|
20540
|
+
try {
|
|
20541
|
+
const query = new URLSearchParams(window.location?.search ?? '');
|
|
20542
|
+
return toPortalFrame(query.get('DOMAIN'), query.get('PROTOCOL'), query.get('APP_SID'));
|
|
20543
|
+
}
|
|
20544
|
+
catch {
|
|
20545
|
+
return undefined;
|
|
20546
|
+
}
|
|
20547
|
+
}
|
|
20548
|
+
function toPortalFrame(rawDomain, rawProtocol, appSid) {
|
|
20549
|
+
// Как BX24.js: порт по умолчанию отрезается, PROTOCOL 1 — https
|
|
20550
|
+
const domain = (rawDomain ?? '').replace(/:(80|443)$/, '');
|
|
20551
|
+
if (!domain || !appSid) {
|
|
20552
|
+
return undefined;
|
|
20553
|
+
}
|
|
20554
|
+
const protocol = (Number.parseInt(rawProtocol ?? '', 10) || 0) ? 'https' : 'http';
|
|
20555
|
+
try {
|
|
20556
|
+
const url = new URL(`${protocol}://${domain}`);
|
|
20557
|
+
// В DOMAIN должен быть только хост (и порт) — никаких путей и логинов
|
|
20558
|
+
if (url.host !== domain.toLowerCase() || url.username || url.password) {
|
|
20559
|
+
return undefined;
|
|
20560
|
+
}
|
|
20561
|
+
return { domain, origin: url.origin, appSid };
|
|
20562
|
+
}
|
|
20563
|
+
catch {
|
|
20564
|
+
return undefined;
|
|
20565
|
+
}
|
|
20566
|
+
}
|
|
20567
|
+
function uniqId() {
|
|
20568
|
+
const bytes = new Uint8Array(16);
|
|
20569
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
20570
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
20571
|
+
}
|
|
20572
|
+
|
|
20573
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
20574
|
+
/**
|
|
20575
|
+
* Команды порталу Bitrix24 из iframe приложения — без подключения BX24.js.
|
|
20576
|
+
* Те же команды и те же имена, что у BX24.js, но вместо колбэков —
|
|
20577
|
+
* промисы. Протокол внутренний и недокументированный, см. `transport.ts`.
|
|
20578
|
+
*
|
|
20579
|
+
* Создавайте один экземпляр на страницу и при старте приложения: параметры
|
|
20580
|
+
* портала читаются в конструкторе (до того, как роутер перепишет адрес), а
|
|
20581
|
+
* `firstRun` портал сообщает только в первом ответе на `getInitData`.
|
|
20582
|
+
*
|
|
20583
|
+
* Токены класс не логирует и никуда не сохраняет: `getAuth()` и
|
|
20584
|
+
* `refreshAuth()` каждый раз спрашивают портал, а в `init()` токенов нет.
|
|
20585
|
+
*
|
|
20586
|
+
* @example
|
|
20587
|
+
* const frame = new BXRestFrame()
|
|
20588
|
+
* const { lang, placement } = await frame.init()
|
|
20589
|
+
* await frame.fitWindow()
|
|
20590
|
+
* const user = await frame.selectUser()
|
|
20591
|
+
*/
|
|
20592
|
+
class BXRestFrame {
|
|
20593
|
+
constructor(options = {}) {
|
|
20594
|
+
this.transport = new FrameTransport();
|
|
20595
|
+
// Открытый диалог каждого вида — новый вызов того же диалога отменяет прошлый
|
|
20596
|
+
this.dialogs = new Map();
|
|
20597
|
+
/** Мессенджер — как `BX24.im`. Портал на эти команды не отвечает. */
|
|
20598
|
+
this.im = {
|
|
20599
|
+
/** Позвонить сотруднику — `BX24.im.callTo()`. */
|
|
20600
|
+
callTo: async (userId, video) => {
|
|
20601
|
+
this.transport.post('imCallTo', { userId, video });
|
|
20602
|
+
},
|
|
20603
|
+
/** Позвонить на телефон — `BX24.im.phoneTo()`. */
|
|
20604
|
+
phoneTo: async (phone) => {
|
|
20605
|
+
this.transport.post('imPhoneTo', { phone });
|
|
20606
|
+
},
|
|
20607
|
+
/** Открыть чат — `BX24.im.openMessenger()`. */
|
|
20608
|
+
openMessenger: async (dialogId) => {
|
|
20609
|
+
this.transport.post('imOpenMessenger', { dialogId });
|
|
20610
|
+
},
|
|
20611
|
+
/** Открыть историю чата — `BX24.im.openHistory()`. */
|
|
20612
|
+
openHistory: async (dialogId) => {
|
|
20613
|
+
this.transport.post('imOpenHistory', { dialogId });
|
|
20614
|
+
},
|
|
20615
|
+
};
|
|
20616
|
+
/** Встраивание — как `BX24.placement`. */
|
|
20617
|
+
this.placement = {
|
|
20618
|
+
/** Код и параметры встраивания — `BX24.placement.info()`. */
|
|
20619
|
+
info: async () => {
|
|
20620
|
+
const { placement, placementOptions } = await this.init();
|
|
20621
|
+
return { placement, options: placementOptions };
|
|
20622
|
+
},
|
|
20623
|
+
/** Команды и события текущего встраивания — `BX24.placement.getInterface()`. */
|
|
20624
|
+
getInterface: async () => {
|
|
20625
|
+
return await this.transport.request('getInterface', null, { timeout: this.timeout });
|
|
20626
|
+
},
|
|
20627
|
+
/**
|
|
20628
|
+
* Команда встраивания — `BX24.placement.call()`. Набор команд зависит от
|
|
20629
|
+
* встраивания (см. `getInterface()`). Не на все команды портал отвечает:
|
|
20630
|
+
* без ответа за `timeout` промис завершается с `undefined`.
|
|
20631
|
+
*/
|
|
20632
|
+
call: async (command, params, options = {}) => {
|
|
20633
|
+
return this.transport.request(command, params, {
|
|
20634
|
+
timeout: options.timeout ?? this.timeout,
|
|
20635
|
+
onTimeout: 'resolve',
|
|
20636
|
+
});
|
|
20637
|
+
},
|
|
20638
|
+
/**
|
|
20639
|
+
* Подписка на событие встраивания — `BX24.placement.bindEvent()`.
|
|
20640
|
+
* `handler` вызывается при каждом событии. Возвращает функцию отписки:
|
|
20641
|
+
* она только перестаёт вызывать `handler` — отменить подписку на
|
|
20642
|
+
* стороне портала протокол не позволяет. Вне iframe бросает
|
|
20643
|
+
* `BXRestFrameError` с кодом `NOT_IN_FRAME`.
|
|
20644
|
+
*/
|
|
20645
|
+
bindEvent: (event, handler) => {
|
|
20646
|
+
return this.transport.subscribe('placementBindEvent', { event }, handler);
|
|
20647
|
+
},
|
|
20648
|
+
};
|
|
20649
|
+
/** Настройки приложения для текущего пользователя — как `BX24.userOption`. */
|
|
20650
|
+
this.userOption = {
|
|
20651
|
+
get: async (name) => (await this.init()).userOptions[name],
|
|
20652
|
+
set: async (name, value) => {
|
|
20653
|
+
const init = await this.init();
|
|
20654
|
+
init.userOptions[name] = value;
|
|
20655
|
+
await this.ack('setUserOption', { name, value });
|
|
20656
|
+
},
|
|
20657
|
+
};
|
|
20658
|
+
/** Настройки приложения для всего портала — как `BX24.appOption`. */
|
|
20659
|
+
this.appOption = {
|
|
20660
|
+
get: async (name) => (await this.init()).appOptions[name],
|
|
20661
|
+
/**
|
|
20662
|
+
* Менять их может только администратор портала: остальным — ошибка
|
|
20663
|
+
* `ACCESS_DENIED` (BX24.js в этом случае молча ничего не делает).
|
|
20664
|
+
* Возвращает настройки приложения после сохранения.
|
|
20665
|
+
*/
|
|
20666
|
+
set: async (name, value) => {
|
|
20667
|
+
const init = await this.init();
|
|
20668
|
+
if (!init.isAdmin) {
|
|
20669
|
+
throw new BXRestFrameError('ACCESS_DENIED', 'Only a Bitrix24 administrator can change application options.');
|
|
20670
|
+
}
|
|
20671
|
+
init.appOptions[name] = value;
|
|
20672
|
+
return await this.transport.request('setAppOption', { name, value }, { timeout: this.timeout });
|
|
20673
|
+
},
|
|
20674
|
+
};
|
|
20675
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
20676
|
+
}
|
|
20677
|
+
/**
|
|
20678
|
+
* Данные запуска приложения — аналог `BX24.init()`: язык, домен, права,
|
|
20679
|
+
* встраивание, настройки, признаки установки. Запрашиваются у портала
|
|
20680
|
+
* один раз, дальше берутся из памяти. Токенов здесь нет — см. `getAuth()`.
|
|
20681
|
+
*/
|
|
20682
|
+
init() {
|
|
20683
|
+
if (this.initValue) {
|
|
20684
|
+
return Promise.resolve(this.initValue);
|
|
20685
|
+
}
|
|
20686
|
+
if (!this.initRequest) {
|
|
20687
|
+
this.initRequest = this.getInitData()
|
|
20688
|
+
.then(() => this.initValue)
|
|
20689
|
+
.finally(() => {
|
|
20690
|
+
this.initRequest = undefined;
|
|
20691
|
+
});
|
|
20692
|
+
}
|
|
20693
|
+
return this.initRequest;
|
|
20694
|
+
}
|
|
20695
|
+
/**
|
|
20696
|
+
* Текущая авторизация приложения — аналог `BX24.getAuth()`. Каждый вызов —
|
|
20697
|
+
* новый запрос `getInitData` к порталу.
|
|
20698
|
+
*
|
|
20699
|
+
* `expires_in` портал считает от срока жизни токена в момент выдачи, а не
|
|
20700
|
+
* от остатка: если страница открыта давно, значение будет завышено (так
|
|
20701
|
+
* же и у BX24.js). Для свежего токена используйте `refreshAuth()`.
|
|
20702
|
+
*/
|
|
20703
|
+
async getAuth() {
|
|
20704
|
+
const data = await this.getInitData();
|
|
20705
|
+
return this.toAuth('getInitData', data, String(data['MEMBER_ID'] || ''));
|
|
20706
|
+
}
|
|
20707
|
+
/**
|
|
20708
|
+
* Новая авторизация приложения — аналог `BX24.refreshAuth()`: портал
|
|
20709
|
+
* выпускает новую пару `access_token`/`refresh_token`. Если портал не может
|
|
20710
|
+
* выдать токен, он показывает `alert` «Unable to get new token!» и не
|
|
20711
|
+
* отвечает — тогда ошибка `TIMEOUT`.
|
|
20712
|
+
*/
|
|
20713
|
+
async refreshAuth() {
|
|
20714
|
+
// member_id приходит только в getInitData — refreshAuth его не возвращает
|
|
20715
|
+
const { memberId } = await this.init();
|
|
20716
|
+
const data = asObject('refreshAuth', await this.transport.request('refreshAuth', {}, { timeout: this.timeout }));
|
|
20717
|
+
return this.toAuth('refreshAuth', data, memberId);
|
|
20718
|
+
}
|
|
20719
|
+
/**
|
|
20720
|
+
* Завершить установку — аналог `BX24.installFinish()`: портал отмечает
|
|
20721
|
+
* приложение установленным и перезагружает страницу. Делает что-то только
|
|
20722
|
+
* на странице установщика (`init().install`), иначе — ничего, как и BX24.js.
|
|
20723
|
+
*/
|
|
20724
|
+
async installFinish() {
|
|
20725
|
+
if ((await this.init()).install) {
|
|
20726
|
+
this.transport.post('setInstallFinish', {});
|
|
20727
|
+
}
|
|
20728
|
+
}
|
|
20729
|
+
/**
|
|
20730
|
+
* Отметить, что первый запуск у этого пользователя пройден, — то, что
|
|
20731
|
+
* BX24.js делает после последнего обработчика `BX24.install()`. Без этого
|
|
20732
|
+
* `init().firstRun` останется `true` и при следующих запусках. Делает
|
|
20733
|
+
* что-то только при первом запуске.
|
|
20734
|
+
*/
|
|
20735
|
+
async firstRunFinish() {
|
|
20736
|
+
const init = await this.init();
|
|
20737
|
+
if (init.firstRun) {
|
|
20738
|
+
await this.ack('setInstall', { install: true });
|
|
20739
|
+
init.firstRun = false;
|
|
20740
|
+
}
|
|
20741
|
+
}
|
|
20742
|
+
/** Задать размер фрейма — `BX24.resizeWindow()`. Возвращает размер, который выставил портал. */
|
|
20743
|
+
async resizeWindow(width, height) {
|
|
20744
|
+
const w = Number.parseInt(String(width), 10);
|
|
20745
|
+
const h = Number.parseInt(String(height), 10);
|
|
20746
|
+
if (!(w > 0 && h > 0)) {
|
|
20747
|
+
throw new TypeError('resizeWindow: width and height must be positive numbers.');
|
|
20748
|
+
}
|
|
20749
|
+
return await this.ack('resizeWindow', { width: w, height: h });
|
|
20750
|
+
}
|
|
20751
|
+
/** Подогнать высоту фрейма под содержимое — `BX24.fitWindow()`. */
|
|
20752
|
+
async fitWindow() {
|
|
20753
|
+
const root = document.documentElement;
|
|
20754
|
+
const height = Math.max(root.scrollHeight, root.offsetHeight);
|
|
20755
|
+
return await this.ack('resizeWindow', { width: '100%', height });
|
|
20756
|
+
}
|
|
20757
|
+
/**
|
|
20758
|
+
* Перезагрузить страницу портала с приложением — `BX24.reloadWindow()`.
|
|
20759
|
+
* Портал на эту команду не отвечает.
|
|
20760
|
+
*/
|
|
20761
|
+
async reloadWindow() {
|
|
20762
|
+
this.transport.post('reloadWindow', {});
|
|
20763
|
+
}
|
|
20764
|
+
/** Заголовок страницы портала — `BX24.setTitle()`. */
|
|
20765
|
+
async setTitle(title) {
|
|
20766
|
+
return await this.ack('setTitle', { title: String(title) });
|
|
20767
|
+
}
|
|
20768
|
+
/** Прокрутить страницу портала — `BX24.scrollParentWindow()`. */
|
|
20769
|
+
async scrollParentWindow(scroll) {
|
|
20770
|
+
const value = Number.parseInt(String(scroll), 10);
|
|
20771
|
+
if (Number.isNaN(value)) {
|
|
20772
|
+
throw new TypeError('scrollParentWindow: scroll must be a number.');
|
|
20773
|
+
}
|
|
20774
|
+
return await this.ack('setScroll', { scroll: value });
|
|
20775
|
+
}
|
|
20776
|
+
/**
|
|
20777
|
+
* Диалог выбора одного сотрудника — `BX24.selectUser()`. Ждёт выбора без
|
|
20778
|
+
* ограничения по времени: если пользователь закрыл диалог, портал не
|
|
20779
|
+
* отвечает вовсе. Отменить ожидание — `signal` или новый вызов
|
|
20780
|
+
* `selectUser()`/`selectUsers()` (ошибка `CANCELLED`).
|
|
20781
|
+
*/
|
|
20782
|
+
selectUser(options = {}) {
|
|
20783
|
+
return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: false }, options.signal);
|
|
20784
|
+
}
|
|
20785
|
+
/** Диалог выбора нескольких сотрудников — `BX24.selectUsers()`. Ожидание — как у `selectUser()`. */
|
|
20786
|
+
selectUsers(options = {}) {
|
|
20787
|
+
return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: true }, options.signal);
|
|
20788
|
+
}
|
|
20789
|
+
/**
|
|
20790
|
+
* Диалог выбора прав доступа — `BX24.selectAccess()`. `value` — коды,
|
|
20791
|
+
* которые нельзя выбрать. Ожидание — как у `selectUser()`.
|
|
20792
|
+
*/
|
|
20793
|
+
selectAccess(options = {}) {
|
|
20794
|
+
return this.dialog('access', 'selectAccess', { value: options.value ?? [] }, options.signal);
|
|
20795
|
+
}
|
|
20796
|
+
/** Диалог выбора объектов CRM — `BX24.selectCRM()`. Ожидание — как у `selectUser()`. */
|
|
20797
|
+
selectCRM(options = {}) {
|
|
20798
|
+
const { entityType, multiple, value, signal } = options;
|
|
20799
|
+
return this.dialog('crm', 'selectCRM', { entityType, multiple, value }, signal);
|
|
20800
|
+
}
|
|
20801
|
+
/**
|
|
20802
|
+
* Открыть приложение в слайдере — `BX24.openApplication()`. Промис
|
|
20803
|
+
* завершается, когда слайдер закрыли. `settings` — настройки слайдера
|
|
20804
|
+
* (уходят как `bx24_<ключ>`, как в BX24.js).
|
|
20805
|
+
*/
|
|
20806
|
+
async openApplication(params = {}, settings = {}, options = {}) {
|
|
20807
|
+
const message = { ...params };
|
|
20808
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
20809
|
+
message[`bx24_${key}`] = value;
|
|
20810
|
+
}
|
|
20811
|
+
await this.transport.request('openApplication', message, { timeout: false, signal: options.signal });
|
|
20812
|
+
}
|
|
20813
|
+
/**
|
|
20814
|
+
* Открыть страницу портала в слайдере — `BX24.openPath()`. Промис
|
|
20815
|
+
* завершается, когда слайдер закрыли (`{ result: 'close' }`), или сразу,
|
|
20816
|
+
* если путь не открылся (`{ result: 'error', errorCode }`).
|
|
20817
|
+
*/
|
|
20818
|
+
async openPath(path, options = {}) {
|
|
20819
|
+
return await this.transport.request('openPath', { path }, { timeout: false, signal: options.signal });
|
|
20820
|
+
}
|
|
20821
|
+
/**
|
|
20822
|
+
* Закрыть слайдер с приложением — `BX24.closeApplication()`. Если
|
|
20823
|
+
* приложение открыто не в слайдере, ничего не происходит. Узнать о
|
|
20824
|
+
* закрытии может открывший слайдер — через `openApplication()`.
|
|
20825
|
+
*/
|
|
20826
|
+
async closeApplication() {
|
|
20827
|
+
this.transport.post('closeApplication', null);
|
|
20828
|
+
}
|
|
20829
|
+
/**
|
|
20830
|
+
* Команда, на которую портал отвечает подтверждением. В некоторых
|
|
20831
|
+
* встраиваниях портал на такие команды не отвечает (так считает и
|
|
20832
|
+
* b24jssdk) — тогда промис завершается с `undefined`, а не ошибкой.
|
|
20833
|
+
*/
|
|
20834
|
+
ack(command, params) {
|
|
20835
|
+
return this.transport.request(command, params, { timeout: this.timeout, onTimeout: 'resolve' });
|
|
20836
|
+
}
|
|
20837
|
+
/**
|
|
20838
|
+
* Диалоги ждут пользователя без таймаута. Портал держит один обработчик
|
|
20839
|
+
* на диалог (у выбора сотрудников он общий для `selectUser` и
|
|
20840
|
+
* `selectUsers`) и на прошлый вызов уже не ответит — поэтому новый вызов
|
|
20841
|
+
* отменяет ожидание прошлого.
|
|
20842
|
+
*/
|
|
20843
|
+
dialog(kind, command, params, signal) {
|
|
20844
|
+
this.dialogs.get(kind)?.abort();
|
|
20845
|
+
const controller = new AbortController();
|
|
20846
|
+
this.dialogs.set(kind, controller);
|
|
20847
|
+
const onAbort = () => controller.abort();
|
|
20848
|
+
if (signal?.aborted) {
|
|
20849
|
+
controller.abort();
|
|
20850
|
+
}
|
|
20851
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
20852
|
+
return this.transport.request(command, params, { timeout: false, signal: controller.signal })
|
|
20853
|
+
.finally(() => {
|
|
20854
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20855
|
+
if (this.dialogs.get(kind) === controller) {
|
|
20856
|
+
this.dialogs.delete(kind);
|
|
20857
|
+
}
|
|
20858
|
+
});
|
|
20859
|
+
}
|
|
20860
|
+
async getInitData() {
|
|
20861
|
+
const data = asObject('getInitData', await this.transport.request('getInitData', null, { timeout: this.timeout }));
|
|
20862
|
+
// firstRun портал отдаёт только в первом ответе за загрузку страницы —
|
|
20863
|
+
// поэтому запоминаем данные первого ответа, кто бы его ни запросил
|
|
20864
|
+
if (!this.initValue) {
|
|
20865
|
+
this.initValue = this.toInit(data);
|
|
20866
|
+
}
|
|
20867
|
+
return data;
|
|
20868
|
+
}
|
|
20869
|
+
toInit(data) {
|
|
20870
|
+
return {
|
|
20871
|
+
// Как BX24.js: домен — из параметров фрейма, DOMAIN ответа — запасной
|
|
20872
|
+
domain: this.transport.domain || String(data['DOMAIN'] || ''),
|
|
20873
|
+
lang: String(data['LANG'] || ''),
|
|
20874
|
+
path: String(data['PATH'] || ''),
|
|
20875
|
+
https: Number.parseInt(String(data['PROTOCOL']), 10) === 1,
|
|
20876
|
+
memberId: String(data['MEMBER_ID'] || ''),
|
|
20877
|
+
isAdmin: !!data['IS_ADMIN'],
|
|
20878
|
+
install: !!data['INSTALL'],
|
|
20879
|
+
firstRun: !!data['FIRST_RUN'],
|
|
20880
|
+
placement: String(data['PLACEMENT'] || 'DEFAULT'),
|
|
20881
|
+
placementOptions: data['PLACEMENT_OPTIONS'],
|
|
20882
|
+
userOptions: toRecord(data['USER_OPTIONS']),
|
|
20883
|
+
appOptions: toRecord(data['APP_OPTIONS']),
|
|
20884
|
+
};
|
|
20885
|
+
}
|
|
20886
|
+
toAuth(command, data, memberId) {
|
|
20887
|
+
const { AUTH_ID, REFRESH_ID, AUTH_EXPIRES } = data;
|
|
20888
|
+
if (!AUTH_ID || typeof AUTH_ID !== 'string') {
|
|
20889
|
+
throw new BXRestFrameError('NO_AUTH', `Bitrix24 answered ${command} without an access token.`);
|
|
20890
|
+
}
|
|
20891
|
+
// AUTH_EXPIRES — срок жизни токена в секундах. BX24.js переводит его в
|
|
20892
|
+
// момент истечения в миллисекундах — так же, как здесь.
|
|
20893
|
+
const expiresIn = Number.parseInt(String(AUTH_EXPIRES), 10);
|
|
20894
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
20895
|
+
throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} without a usable AUTH_EXPIRES.`);
|
|
20896
|
+
}
|
|
20897
|
+
return {
|
|
20898
|
+
access_token: AUTH_ID,
|
|
20899
|
+
refresh_token: typeof REFRESH_ID === 'string' ? REFRESH_ID : '',
|
|
20900
|
+
expires_in: Date.now() + expiresIn * 1000,
|
|
20901
|
+
domain: this.transport.domain,
|
|
20902
|
+
member_id: memberId,
|
|
20903
|
+
};
|
|
20904
|
+
}
|
|
20905
|
+
}
|
|
20906
|
+
function asObject(command, data) {
|
|
20907
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
20908
|
+
throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not an object.`);
|
|
20909
|
+
}
|
|
20910
|
+
return data;
|
|
20911
|
+
}
|
|
20912
|
+
// Пустые настройки PHP отдаёт массивом `[]`
|
|
20913
|
+
function toRecord(value) {
|
|
20914
|
+
return (value && typeof value === 'object' && !Array.isArray(value))
|
|
20915
|
+
? { ...value }
|
|
20916
|
+
: {};
|
|
20917
|
+
}
|
|
20918
|
+
|
|
20200
20919
|
/**
|
|
20201
20920
|
* Public API Surface of bx-rest
|
|
20202
20921
|
*/
|
|
@@ -20205,5 +20924,5 @@ class BXRestMap {
|
|
|
20205
20924
|
* Generated bundle index. Do not edit.
|
|
20206
20925
|
*/
|
|
20207
20926
|
|
|
20208
|
-
export { base as BXBaseServices, BXRestApiError, BXRestMap, BXRestMapResult, BXRestNavvy, HttpBXServices as BXRestRequest, BXRestSettings, HttpNavvyServices, Navvy, SessionKeyError, SessionKeyServices, instanceOfiBXRestAnswerSuccess as isBXRestAnswerSuccess, partNameMethods$1 as partNameMethods };
|
|
20927
|
+
export { base as BXBaseServices, BXRestApiError, BXRestFrame, BXRestFrameError, BXRestMap, BXRestMapResult, BXRestNavvy, HttpBXServices as BXRestRequest, BXRestSettings, HttpNavvyServices, Navvy, SessionKeyError, SessionKeyServices, getAuthRefreshError, instanceOfiBXRestAnswerSuccess as isBXRestAnswerSuccess, isBXRestAuthError, partNameMethods$1 as partNameMethods };
|
|
20209
20928
|
//# sourceMappingURL=bx-rest.mjs.map
|