bx-rest 1.3.0 → 1.5.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 +83 -0
- package/README.en.md +207 -3
- package/README.md +207 -3
- package/esm2022/functions/errors.mjs +19 -2
- package/esm2022/public-api.mjs +6 -2
- package/esm2022/request-navvy/crm/item.mjs +1 -1
- package/esm2022/request-navvy/crm/type.mjs +1 -1
- 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 +14 -6
- package/esm2022/services/http/authRefresh.mjs +89 -0
- package/esm2022/services/navvy/extends/navvy-pag-base.mjs +28 -5
- package/esm2022/services/navvy/navvy-alternative-pagination-navigation.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag-nav-result-key-object.mjs +6 -7
- package/esm2022/services/navvy/navvy-pag-nav-result-key.mjs +6 -3
- package/esm2022/services/navvy/navvy-pag-nav-tasks.mjs +5 -4
- 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/rest/crm/universal.mjs +1 -1
- package/esm2022/typification/settings.mjs +1 -1
- package/fesm2022/bx-rest.mjs +801 -97
- package/fesm2022/bx-rest.mjs.map +1 -1
- package/functions/errors.d.ts +18 -0
- package/package.json +4 -1
- package/public-api.d.ts +5 -1
- package/request-navvy/crm/item.d.ts +6 -6
- package/request-navvy/crm/type.d.ts +6 -6
- 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/authRefresh.d.ts +19 -0
- package/services/navvy/extends/navvy-pag-base.d.ts +6 -0
- package/services/navvy/navvy-pag-nav-tasks.d.ts +3 -0
- package/typification/rest/crm/universal.d.ts +116 -0
- package/typification/settings.d.ts +17 -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';
|
|
@@ -930,6 +930,179 @@ function serializeBitrixParams(params) {
|
|
|
930
930
|
return stringify(params, BITRIX_STRINGIFY_OPTIONS);
|
|
931
931
|
}
|
|
932
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
|
+
* `.resAll()` always fails with it on an error in the body of a 2xx response.
|
|
956
|
+
*/
|
|
957
|
+
class BXRestApiError extends Error {
|
|
958
|
+
constructor(error, error_description, details) {
|
|
959
|
+
super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
|
|
960
|
+
this.error = error;
|
|
961
|
+
this.error_description = error_description;
|
|
962
|
+
this.name = 'BXRestApiError';
|
|
963
|
+
this.status = details?.status;
|
|
964
|
+
this.method = details?.method;
|
|
965
|
+
this.authRefreshError = details?.authRefreshError;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
|
|
970
|
+
* REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
|
|
971
|
+
* else — a success payload, an HTML page from a proxy, an empty body.
|
|
972
|
+
*
|
|
973
|
+
* An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
|
|
974
|
+
* missing ID with `{ error: '', error_description: 'Not found' }`.
|
|
975
|
+
*/
|
|
976
|
+
function parseBXRestErrorBody(body) {
|
|
977
|
+
if (!body || typeof body !== 'object' || !('error' in body)) {
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
const { error, error_description } = body;
|
|
981
|
+
if (typeof error === 'string') {
|
|
982
|
+
return {
|
|
983
|
+
error,
|
|
984
|
+
error_description: typeof error_description === 'string' ? error_description : undefined
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
988
|
+
const { code, message } = error;
|
|
989
|
+
return {
|
|
990
|
+
error: String(code),
|
|
991
|
+
error_description: typeof message === 'string' ? message : undefined
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Turns an HTTP failure into a `BXRestApiError` when the portal answered with
|
|
998
|
+
* a Bitrix error body. Bitrix sends most errors with a non-2xx status
|
|
999
|
+
* (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
|
|
1000
|
+
* `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
|
|
1001
|
+
* rejects on those.
|
|
1002
|
+
*
|
|
1003
|
+
* Returns `undefined` for network errors and for responses that are not a
|
|
1004
|
+
* Bitrix error body, so the caller can keep its own handling for them.
|
|
1005
|
+
*/
|
|
1006
|
+
function bxRestApiErrorFromHttp(err, method) {
|
|
1007
|
+
if (!axios.isAxiosError(err) || !err.response) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
const parsed = parseBXRestErrorBody(err.response.data);
|
|
1011
|
+
if (!parsed) {
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
return new BXRestApiError(parsed.error, parsed.error_description, {
|
|
1015
|
+
status: err.response.status,
|
|
1016
|
+
method,
|
|
1017
|
+
cause: err,
|
|
1018
|
+
authRefreshError: getAuthRefreshError(err)
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Коды ошибок Bitrix, которые лечатся продлением токена. `expired_token` —
|
|
1024
|
+
* единственный системный код, для которого документация Bitrix24 советует
|
|
1025
|
+
* "обновить токены и повторить вызов". `invalid_token` в списке системных
|
|
1026
|
+
* ошибок документации нет, но так же считает официальный `@bitrix24/b24jssdk`.
|
|
1027
|
+
*
|
|
1028
|
+
* Остальные системные ошибки тоже приходят со статусом 401
|
|
1029
|
+
* (`insufficient_scope`, `NO_AUTH_FOUND`, `ACCESS_DENIED`,
|
|
1030
|
+
* `INVALID_CREDENTIALS`, `user_access_error`, ...), но новый токен их
|
|
1031
|
+
* не исправит — поэтому смотрим на код, а не только на статус.
|
|
1032
|
+
*/
|
|
1033
|
+
const AUTH_ERROR_CODES = ['expired_token', 'invalid_token'];
|
|
1034
|
+
/**
|
|
1035
|
+
* Условие продления по умолчанию (`BXRestSettings.auth.isAuthError`):
|
|
1036
|
+
* - ответ с ошибкой Bitrix `expired_token` или `invalid_token` — при любом статусе;
|
|
1037
|
+
* - ответ 401 без тела ошибки Bitrix — например, пустой ответ своего
|
|
1038
|
+
* бэкенд-прокси, у которого нет действующего токена.
|
|
1039
|
+
*
|
|
1040
|
+
* Ответ 401 с другим кодом Bitrix ошибкой авторизации не считается.
|
|
1041
|
+
*/
|
|
1042
|
+
function isBXRestAuthError(error) {
|
|
1043
|
+
if (!axios.isAxiosError(error) || !error.response) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
const parsed = parseBXRestErrorBody(error.response.data);
|
|
1047
|
+
if (parsed) {
|
|
1048
|
+
return AUTH_ERROR_CODES.includes(parsed.error);
|
|
1049
|
+
}
|
|
1050
|
+
return error.response.status === 401;
|
|
1051
|
+
}
|
|
1052
|
+
// Общее на всю библиотеку состояние продления — как и сами BXRestSettings.
|
|
1053
|
+
// pendingRefresh — идущее сейчас продление, его ждут все упавшие запросы;
|
|
1054
|
+
// refreshCount — сколько продлений уже завершилось успешно.
|
|
1055
|
+
let pendingRefresh;
|
|
1056
|
+
let refreshCount = 0;
|
|
1057
|
+
function sharedRefresh(refresh) {
|
|
1058
|
+
if (!pendingRefresh) {
|
|
1059
|
+
pendingRefresh = Promise.resolve()
|
|
1060
|
+
// Через then, чтобы синхронное исключение в refresh() тоже стало
|
|
1061
|
+
// отклонённым промисом, а не вылетело из catchError
|
|
1062
|
+
.then(() => refresh())
|
|
1063
|
+
.then(() => {
|
|
1064
|
+
refreshCount++;
|
|
1065
|
+
})
|
|
1066
|
+
.finally(() => {
|
|
1067
|
+
pendingRefresh = undefined;
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
return pendingRefresh;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Повтор запроса после продления авторизации (`BXRestSettings.auth.refresh`).
|
|
1074
|
+
*
|
|
1075
|
+
* `request$` должен быть "холодным" и читать настройки при подписке (как
|
|
1076
|
+
* `defer()` в HttpBXServices): повтор — это повторная подписка, поэтому
|
|
1077
|
+
* токен, адрес и заголовки берутся уже новые. На второй попытке ошибка не
|
|
1078
|
+
* перехватывается — запрос повторяется не больше одного раза.
|
|
1079
|
+
*/
|
|
1080
|
+
function withAuthRefresh(request$) {
|
|
1081
|
+
return defer(() => {
|
|
1082
|
+
const { refresh, isAuthError = isBXRestAuthError } = BXRestSettings.value.auth;
|
|
1083
|
+
if (!refresh) {
|
|
1084
|
+
return request$;
|
|
1085
|
+
}
|
|
1086
|
+
const refreshCountAtStart = refreshCount;
|
|
1087
|
+
return request$.pipe(catchError((error) => {
|
|
1088
|
+
if (!isAuthError(error)) {
|
|
1089
|
+
return throwError(() => error);
|
|
1090
|
+
}
|
|
1091
|
+
// Пока этот запрос был в пути, токен уже продлили по ошибке другого
|
|
1092
|
+
// запроса — повторяем без нового продления
|
|
1093
|
+
const refreshed = (refreshCount !== refreshCountAtStart)
|
|
1094
|
+
? Promise.resolve()
|
|
1095
|
+
: sharedRefresh(refresh);
|
|
1096
|
+
return from(refreshed).pipe(catchError((refreshError) => {
|
|
1097
|
+
if (error && typeof error === 'object') {
|
|
1098
|
+
error.authRefreshError = refreshError;
|
|
1099
|
+
}
|
|
1100
|
+
return throwError(() => error);
|
|
1101
|
+
}), switchMap(() => request$));
|
|
1102
|
+
}));
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
933
1106
|
class HttpBXServices extends HttpServices {
|
|
934
1107
|
/**
|
|
935
1108
|
* Не считайте этот метод образцом/финальным API.
|
|
@@ -1002,7 +1175,13 @@ class HttpBXServices extends HttpServices {
|
|
|
1002
1175
|
// геттером в SessionKeyServices настройки читались бы в момент вызова
|
|
1003
1176
|
// .res(), а не в момент subscribe()/firstValueFrom(), что и обещано
|
|
1004
1177
|
// в README как поведение "ленивого" REST-вызова.
|
|
1005
|
-
|
|
1178
|
+
//
|
|
1179
|
+
// withAuthRefresh() повторяет запрос после BXRestSettings.auth.refresh —
|
|
1180
|
+
// повторной подпиской на этот же defer(), поэтому повтор заново читает
|
|
1181
|
+
// токен, адрес и заголовки. Через httpPost/httpGet идут все запросы
|
|
1182
|
+
// библиотеки (post/get, batch, все виды Navvy-запросов), так что повтор
|
|
1183
|
+
// встроен только здесь.
|
|
1184
|
+
return withAuthRefresh(defer(() => {
|
|
1006
1185
|
const paramsClone = clone(params);
|
|
1007
1186
|
const key = this.session.getKeyAuth();
|
|
1008
1187
|
const auth = this.session.getAuthParams();
|
|
@@ -1032,14 +1211,15 @@ class HttpBXServices extends HttpServices {
|
|
|
1032
1211
|
headers: this.buildHeaders(headers, { 'Content-Type': 'application/json' }),
|
|
1033
1212
|
}).then((response) => response.data));
|
|
1034
1213
|
}));
|
|
1035
|
-
});
|
|
1214
|
+
}));
|
|
1036
1215
|
}
|
|
1037
1216
|
httpGet(url, params = {}, settings, headers) {
|
|
1038
1217
|
// Та же причина, что и у httpPost выше: откладываем чтение BXRestSettings
|
|
1039
1218
|
// до подписки, чтобы `get()` вёл себя так же "лениво", как и `post()`
|
|
1040
1219
|
// (симметрия, которой не было до этого — GET резолвил настройки в момент
|
|
1041
|
-
// вызова, а не в момент subscribe()).
|
|
1042
|
-
|
|
1220
|
+
// вызова, а не в момент subscribe()). Повтор после продления
|
|
1221
|
+
// авторизации — как в httpPost.
|
|
1222
|
+
return withAuthRefresh(defer(() => {
|
|
1043
1223
|
const paramsClone = clone(params);
|
|
1044
1224
|
const auth = this.session.getAuthParams();
|
|
1045
1225
|
const checkIsOn = this.session.getCheckAuthParamsIsOn();
|
|
@@ -1060,7 +1240,7 @@ class HttpBXServices extends HttpServices {
|
|
|
1060
1240
|
headers: this.buildHeaders(headers),
|
|
1061
1241
|
}).then((response) => response.data));
|
|
1062
1242
|
}));
|
|
1063
|
-
});
|
|
1243
|
+
}));
|
|
1064
1244
|
}
|
|
1065
1245
|
/**
|
|
1066
1246
|
* Заголовки запроса: `BXRestSettings.headers`, поверх них — заголовки
|
|
@@ -1079,78 +1259,6 @@ class HttpBXServices extends HttpServices {
|
|
|
1079
1259
|
}
|
|
1080
1260
|
}
|
|
1081
1261
|
|
|
1082
|
-
/**
|
|
1083
|
-
* A Bitrix-level API error, e.g. `INSUFFICIENT_SCOPE` or `expired_token`.
|
|
1084
|
-
*
|
|
1085
|
-
* `.res()` fails with it whenever the portal answers with a Bitrix error and
|
|
1086
|
-
* a non-2xx HTTP status — which is how Bitrix sends most of its errors. When
|
|
1087
|
-
* the error comes in the body of a 2xx response, `.res()` resolves as
|
|
1088
|
-
* `undefined` by default and fails with it only with
|
|
1089
|
-
* `{ throwOnApiError: true }` — see the "Error handling" section of the README.
|
|
1090
|
-
*/
|
|
1091
|
-
class BXRestApiError extends Error {
|
|
1092
|
-
constructor(error, error_description, details) {
|
|
1093
|
-
super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
|
|
1094
|
-
this.error = error;
|
|
1095
|
-
this.error_description = error_description;
|
|
1096
|
-
this.name = 'BXRestApiError';
|
|
1097
|
-
this.status = details?.status;
|
|
1098
|
-
this.method = details?.method;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
/**
|
|
1102
|
-
* Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
|
|
1103
|
-
* REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
|
|
1104
|
-
* else — a success payload, an HTML page from a proxy, an empty body.
|
|
1105
|
-
*
|
|
1106
|
-
* An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
|
|
1107
|
-
* missing ID with `{ error: '', error_description: 'Not found' }`.
|
|
1108
|
-
*/
|
|
1109
|
-
function parseBXRestErrorBody(body) {
|
|
1110
|
-
if (!body || typeof body !== 'object' || !('error' in body)) {
|
|
1111
|
-
return undefined;
|
|
1112
|
-
}
|
|
1113
|
-
const { error, error_description } = body;
|
|
1114
|
-
if (typeof error === 'string') {
|
|
1115
|
-
return {
|
|
1116
|
-
error,
|
|
1117
|
-
error_description: typeof error_description === 'string' ? error_description : undefined
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
if (error && typeof error === 'object' && 'code' in error) {
|
|
1121
|
-
const { code, message } = error;
|
|
1122
|
-
return {
|
|
1123
|
-
error: String(code),
|
|
1124
|
-
error_description: typeof message === 'string' ? message : undefined
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1127
|
-
return undefined;
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Turns an HTTP failure into a `BXRestApiError` when the portal answered with
|
|
1131
|
-
* a Bitrix error body. Bitrix sends most errors with a non-2xx status
|
|
1132
|
-
* (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
|
|
1133
|
-
* `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
|
|
1134
|
-
* rejects on those.
|
|
1135
|
-
*
|
|
1136
|
-
* Returns `undefined` for network errors and for responses that are not a
|
|
1137
|
-
* Bitrix error body, so the caller can keep its own handling for them.
|
|
1138
|
-
*/
|
|
1139
|
-
function bxRestApiErrorFromHttp(err, method) {
|
|
1140
|
-
if (!axios.isAxiosError(err) || !err.response) {
|
|
1141
|
-
return undefined;
|
|
1142
|
-
}
|
|
1143
|
-
const parsed = parseBXRestErrorBody(err.response.data);
|
|
1144
|
-
if (!parsed) {
|
|
1145
|
-
return undefined;
|
|
1146
|
-
}
|
|
1147
|
-
return new BXRestApiError(parsed.error, parsed.error_description, {
|
|
1148
|
-
status: err.response.status,
|
|
1149
|
-
method,
|
|
1150
|
-
cause: err
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
1262
|
class NavvyPagBase {
|
|
1155
1263
|
constructor(url, param, map = undefined, restApi = false) {
|
|
1156
1264
|
this.url = url;
|
|
@@ -1196,8 +1304,7 @@ class NavvyPagBase {
|
|
|
1196
1304
|
const method = this.http.getNameMethod(this.url);
|
|
1197
1305
|
return request.pipe(map(v => {
|
|
1198
1306
|
if (options?.throwOnApiError && v && !instanceOfiBXRestAnswerSuccess(v)) {
|
|
1199
|
-
|
|
1200
|
-
throw new BXRestApiError(parsed ? parsed.error : v.error, parsed ? parsed.error_description : v.error_description, { method });
|
|
1307
|
+
throw this.apiErrorFromBody(v);
|
|
1201
1308
|
}
|
|
1202
1309
|
return BXRestMapResult(v);
|
|
1203
1310
|
}), catchError((err) => {
|
|
@@ -1212,9 +1319,33 @@ class NavvyPagBase {
|
|
|
1212
1319
|
if (apiError) {
|
|
1213
1320
|
throw apiError;
|
|
1214
1321
|
}
|
|
1215
|
-
|
|
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;
|
|
1216
1331
|
}));
|
|
1217
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Первая страница для `.resAll()`. Ошибка Bitrix в теле ответа 2xx становится `BXRestApiError`:
|
|
1335
|
+
* страниц в таком ответе нет, и без этого `.resAll()` завершился бы безликим `EmptyError`.
|
|
1336
|
+
*/
|
|
1337
|
+
resFirstPage() {
|
|
1338
|
+
return this.resVanilla().pipe(map(v => {
|
|
1339
|
+
if (v && !instanceOfiBXRestAnswerSuccess(v)) {
|
|
1340
|
+
throw this.apiErrorFromBody(v);
|
|
1341
|
+
}
|
|
1342
|
+
return v;
|
|
1343
|
+
}));
|
|
1344
|
+
}
|
|
1345
|
+
apiErrorFromBody(v) {
|
|
1346
|
+
const parsed = parseBXRestErrorBody(v);
|
|
1347
|
+
return new BXRestApiError(parsed ? parsed.error : v.error, parsed ? parsed.error_description : v.error_description, { method: this.http.getNameMethod(this.url) });
|
|
1348
|
+
}
|
|
1218
1349
|
}
|
|
1219
1350
|
|
|
1220
1351
|
class NavvySimple extends NavvyPagBase {
|
|
@@ -3165,7 +3296,7 @@ class NavvyAlterPagNav extends NavvyPagBase {
|
|
|
3165
3296
|
resultAllEnd() {
|
|
3166
3297
|
let BXRestClass = new BXRest();
|
|
3167
3298
|
// return this.mapForVanillaEnd(this.func, this.param)
|
|
3168
|
-
return this.
|
|
3299
|
+
return this.resFirstPage().pipe(mergeMap(items => {
|
|
3169
3300
|
if (items && instanceOfiBXRestAnswerSuccess(items)) {
|
|
3170
3301
|
if (items.total && items.next) {
|
|
3171
3302
|
const count = [...Array(Math.floor(items.total / this.pageSize)).keys()];
|
|
@@ -3229,7 +3360,7 @@ class NavvyPagNavWithUselessKey extends NavvyPagBase {
|
|
|
3229
3360
|
}
|
|
3230
3361
|
resAll() {
|
|
3231
3362
|
let BXRestClass = new BXRest();
|
|
3232
|
-
return this.
|
|
3363
|
+
return this.resFirstPage().pipe(mergeMap(items => {
|
|
3233
3364
|
if (items && instanceOfiBXRestAnswerSuccess(items)) {
|
|
3234
3365
|
if (items.total && items.next) {
|
|
3235
3366
|
const count = [...Array(Math.floor(items.total / this.pageSize)).keys()];
|
|
@@ -3297,8 +3428,9 @@ class NavvyPagNavWithUselessKey extends NavvyPagBase {
|
|
|
3297
3428
|
}
|
|
3298
3429
|
|
|
3299
3430
|
class NavvyPagNavTasks extends NavvyPagBase {
|
|
3300
|
-
constructor() {
|
|
3301
|
-
|
|
3431
|
+
constructor(url, param, map, restApi) {
|
|
3432
|
+
// Без своего map результат — массив задач, как и обещают типы `.res()` и `.resAll()`.
|
|
3433
|
+
super(url, param, map ?? (result => (result?.tasks ?? [])), restApi);
|
|
3302
3434
|
this.save = [];
|
|
3303
3435
|
}
|
|
3304
3436
|
resAll() {
|
|
@@ -3313,7 +3445,7 @@ class NavvyPagNavTasks extends NavvyPagBase {
|
|
|
3313
3445
|
}
|
|
3314
3446
|
resultAllEnd() {
|
|
3315
3447
|
let BXRestClass = new BXRest();
|
|
3316
|
-
return this.
|
|
3448
|
+
return this.resFirstPage().pipe(mergeMap(items => {
|
|
3317
3449
|
if (items && instanceOfiBXRestAnswerSuccess(items)) {
|
|
3318
3450
|
this.save = [];
|
|
3319
3451
|
// TODO: проверить будет ли быстрее работать метод в случаях когда total < 100 (то есть можно обойтись двумя прямыми запросами)
|
|
@@ -3374,7 +3506,7 @@ class NavvyPag extends NavvyPagBase {
|
|
|
3374
3506
|
}
|
|
3375
3507
|
resAll() {
|
|
3376
3508
|
let BXRestClass = new BXRest();
|
|
3377
|
-
return this.
|
|
3509
|
+
return this.resFirstPage().pipe(mergeMap(items => {
|
|
3378
3510
|
if (items && instanceOfiBXRestAnswerSuccess(items)) {
|
|
3379
3511
|
this.save = [];
|
|
3380
3512
|
// TODO: проверить будет ли быстрее работать метод в случаях когда total < 100 (то есть можно обойтись двумя прямыми запросами)
|
|
@@ -3432,7 +3564,10 @@ class NavvyPag extends NavvyPagBase {
|
|
|
3432
3564
|
|
|
3433
3565
|
class NavvyPagNavResultKey extends NavvyPagBase {
|
|
3434
3566
|
constructor(url, param, key, map) {
|
|
3435
|
-
|
|
3567
|
+
// Без своего map результат — массив по ключу, как и обещают типы `.res()` и `.resAll()`.
|
|
3568
|
+
// Иначе `.res()` отдал бы весь объект `{ items: [...] }`, а `.resAll()` упал бы на нём
|
|
3569
|
+
// при 50 записях и меньше: первая страница разворачивается через `save.push(...v)`.
|
|
3570
|
+
super(url, param, map ?? (result => (result?.[key] ?? [])));
|
|
3436
3571
|
this.key = key;
|
|
3437
3572
|
this.save = [];
|
|
3438
3573
|
}
|
|
@@ -3448,7 +3583,7 @@ class NavvyPagNavResultKey extends NavvyPagBase {
|
|
|
3448
3583
|
}
|
|
3449
3584
|
resultAllEnd() {
|
|
3450
3585
|
let BXRestClass = new BXRest();
|
|
3451
|
-
return this.
|
|
3586
|
+
return this.resFirstPage().pipe(mergeMap(items => {
|
|
3452
3587
|
if (items && instanceOfiBXRestAnswerSuccess(items)) {
|
|
3453
3588
|
this.save = [];
|
|
3454
3589
|
if (items.total && items.total > 50) {
|
|
@@ -3503,7 +3638,8 @@ class NavvyPagNavResultKey extends NavvyPagBase {
|
|
|
3503
3638
|
/** Пагинация для методов, возвращающих коллекцию как объект с идентификаторами в ключах. */
|
|
3504
3639
|
class NavvyPagNavResultKeyObject extends NavvyPagBase {
|
|
3505
3640
|
constructor(url, param, key, mapResult) {
|
|
3506
|
-
|
|
3641
|
+
// Без своего map результат — значения объекта по ключу, как и обещают типы `.res()` и `.resAll()`.
|
|
3642
|
+
super(url, param, mapResult ?? (result => Object.values(result?.[key] ?? {})));
|
|
3507
3643
|
this.key = key;
|
|
3508
3644
|
}
|
|
3509
3645
|
resAll() {
|
|
@@ -3511,7 +3647,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
|
|
|
3511
3647
|
}
|
|
3512
3648
|
resultAllEnd() {
|
|
3513
3649
|
const bxRest = new BXRest();
|
|
3514
|
-
return this.
|
|
3650
|
+
return this.resFirstPage().pipe(mergeMap(items => {
|
|
3515
3651
|
if (!items || !instanceOfiBXRestAnswerSuccess(items)) {
|
|
3516
3652
|
return from([]);
|
|
3517
3653
|
}
|
|
@@ -3536,7 +3672,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
|
|
|
3536
3672
|
}
|
|
3537
3673
|
}
|
|
3538
3674
|
}
|
|
3539
|
-
return this.mapResult({ [this.key]: merged });
|
|
3675
|
+
return this.mapResult({ result: { [this.key]: merged } });
|
|
3540
3676
|
}));
|
|
3541
3677
|
}), catchError(error => {
|
|
3542
3678
|
console.error(error);
|
|
@@ -3547,9 +3683,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
|
|
|
3547
3683
|
if (!instanceOfiBXRestAnswerSuccess(items)) {
|
|
3548
3684
|
return from([]);
|
|
3549
3685
|
}
|
|
3550
|
-
return from([items.result]).pipe(map$1(value => this.map
|
|
3551
|
-
? this.map(value)
|
|
3552
|
-
: Object.values(value?.[this.key] ?? {})));
|
|
3686
|
+
return from([items.result]).pipe(map$1(value => this.map?.(value)));
|
|
3553
3687
|
}
|
|
3554
3688
|
}
|
|
3555
3689
|
|
|
@@ -20231,6 +20365,576 @@ class BXRestMap {
|
|
|
20231
20365
|
}
|
|
20232
20366
|
}
|
|
20233
20367
|
|
|
20368
|
+
/**
|
|
20369
|
+
* Обмен сообщениями между iframe приложения и порталом Bitrix24 — тот
|
|
20370
|
+
* транспорт, на котором построены команды BX24.js: страница приложения шлёт
|
|
20371
|
+
* команду родительскому окну портала через postMessage, портал отвечает
|
|
20372
|
+
* сообщением с тем же callbackId.
|
|
20373
|
+
*
|
|
20374
|
+
* ВНИМАНИЕ: протокол внутренний и недокументированный — Bitrix24 может
|
|
20375
|
+
* изменить его без предупреждения. Транспорт повторяет BX24.js
|
|
20376
|
+
* (https://api.bitrix24.com/api/v1/, скачан 2026-10-29, Last-Modified
|
|
20377
|
+
* 2022-03-17, sha256 945ad491df8ffb7490b89f4914a439b5a3f575c950c3480562815716a3e07f23)
|
|
20378
|
+
* и сверен с ответной стороной портала — `/bitrix/js/rest/applayout.js`
|
|
20379
|
+
* (Last-Modified 2024-05-15, sha256 3efd6ab57a4778493a26d10ae0987a82e19fc012ed9935f5c6afa78c4052fcb5).
|
|
20380
|
+
* Всё знание о протоколе — только в этом модуле (`services/frame`).
|
|
20381
|
+
*/
|
|
20382
|
+
/**
|
|
20383
|
+
* Ошибка обращения к порталу из iframe. `code`:
|
|
20384
|
+
* - `NOT_IN_FRAME` — страница открыта не в iframe приложения Bitrix24;
|
|
20385
|
+
* - `TIMEOUT` — портал не ответил за отведённое время;
|
|
20386
|
+
* - `NO_AUTH` — портал ответил без токена;
|
|
20387
|
+
* - `BAD_RESPONSE` — ответ портала не удалось разобрать;
|
|
20388
|
+
* - `CANCELLED` — ожидание отменено через `signal` или новым вызовом того же диалога;
|
|
20389
|
+
* - `ACCESS_DENIED` — команда доступна только администратору портала.
|
|
20390
|
+
*/
|
|
20391
|
+
class BXRestFrameError extends Error {
|
|
20392
|
+
constructor(code, message) {
|
|
20393
|
+
super(message);
|
|
20394
|
+
this.code = code;
|
|
20395
|
+
this.name = 'BXRestFrameError';
|
|
20396
|
+
}
|
|
20397
|
+
}
|
|
20398
|
+
/**
|
|
20399
|
+
* Параметры портала (DOMAIN, PROTOCOL, APP_SID) читаются в конструкторе —
|
|
20400
|
+
* так же, как это делает BX24.js, из `window.name`: портал задаёт имя
|
|
20401
|
+
* iframe приложения в виде `DOMAIN|PROTOCOL|APP_SID`. Если там их нет —
|
|
20402
|
+
* из query страницы (`?DOMAIN=…&PROTOCOL=…&APP_SID=…`), с которым портал
|
|
20403
|
+
* открывает обработчик приложения. Конструктор не бросает исключений:
|
|
20404
|
+
* вне iframe ошибку вернут сами команды.
|
|
20405
|
+
*/
|
|
20406
|
+
class FrameTransport {
|
|
20407
|
+
constructor() {
|
|
20408
|
+
this.frame = readPortalFrame();
|
|
20409
|
+
}
|
|
20410
|
+
/** Домен портала из параметров фрейма — как `BX24.getDomain()`. */
|
|
20411
|
+
get domain() {
|
|
20412
|
+
return this.frame?.domain ?? '';
|
|
20413
|
+
}
|
|
20414
|
+
/**
|
|
20415
|
+
* Команда, на которую портал не отвечает (`reloadWindow`, `imCallTo`, ...).
|
|
20416
|
+
* Вне iframe бросает `BXRestFrameError` с кодом `NOT_IN_FRAME`.
|
|
20417
|
+
*/
|
|
20418
|
+
post(command, params) {
|
|
20419
|
+
this.send(this.requireFrame(), command, params, '');
|
|
20420
|
+
}
|
|
20421
|
+
/** Команда с одним ответом портала. */
|
|
20422
|
+
request(command, params, options) {
|
|
20423
|
+
return new Promise((resolve, reject) => {
|
|
20424
|
+
let frame;
|
|
20425
|
+
try {
|
|
20426
|
+
frame = this.requireFrame();
|
|
20427
|
+
}
|
|
20428
|
+
catch (error) {
|
|
20429
|
+
reject(error);
|
|
20430
|
+
return;
|
|
20431
|
+
}
|
|
20432
|
+
const { timeout, onTimeout = 'reject', signal } = options;
|
|
20433
|
+
if (signal?.aborted) {
|
|
20434
|
+
reject(cancelled(command));
|
|
20435
|
+
return;
|
|
20436
|
+
}
|
|
20437
|
+
const callbackId = uniqId();
|
|
20438
|
+
let timer;
|
|
20439
|
+
const finish = () => {
|
|
20440
|
+
clearTimeout(timer);
|
|
20441
|
+
stopListening();
|
|
20442
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20443
|
+
};
|
|
20444
|
+
const onAbort = () => {
|
|
20445
|
+
finish();
|
|
20446
|
+
reject(cancelled(command));
|
|
20447
|
+
};
|
|
20448
|
+
const stopListening = this.listen(frame, callbackId, payload => {
|
|
20449
|
+
finish();
|
|
20450
|
+
const answer = parsePayload(payload);
|
|
20451
|
+
if (answer.ok) {
|
|
20452
|
+
resolve(answer.value);
|
|
20453
|
+
}
|
|
20454
|
+
else {
|
|
20455
|
+
// В тексте ошибки — только команда, без тела ответа: в нём бывают токены
|
|
20456
|
+
reject(new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not JSON.`));
|
|
20457
|
+
}
|
|
20458
|
+
});
|
|
20459
|
+
if (timeout !== false) {
|
|
20460
|
+
timer = setTimeout(() => {
|
|
20461
|
+
finish();
|
|
20462
|
+
if (onTimeout === 'resolve') {
|
|
20463
|
+
resolve(undefined);
|
|
20464
|
+
}
|
|
20465
|
+
else {
|
|
20466
|
+
reject(new BXRestFrameError('TIMEOUT', [
|
|
20467
|
+
`Bitrix24 did not answer ${command} within ${timeout} ms:`,
|
|
20468
|
+
'the page is not opened inside a Bitrix24 portal iframe, or the portal could not process the command.'
|
|
20469
|
+
].join(' ')));
|
|
20470
|
+
}
|
|
20471
|
+
}, timeout);
|
|
20472
|
+
}
|
|
20473
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
20474
|
+
this.send(frame, command, params, callbackId);
|
|
20475
|
+
});
|
|
20476
|
+
}
|
|
20477
|
+
/**
|
|
20478
|
+
* Команда, на которую портал отвечает многократно (`placementBindEvent`):
|
|
20479
|
+
* `handler` вызывается на каждый ответ. Возвращает функцию отписки —
|
|
20480
|
+
* она только перестаёт слушать: отменить подписку на стороне портала
|
|
20481
|
+
* протокол не позволяет. Вне iframe бросает `BXRestFrameError`.
|
|
20482
|
+
*/
|
|
20483
|
+
subscribe(command, params, handler) {
|
|
20484
|
+
const frame = this.requireFrame();
|
|
20485
|
+
const callbackId = uniqId();
|
|
20486
|
+
const stopListening = this.listen(frame, callbackId, payload => {
|
|
20487
|
+
const answer = parsePayload(payload);
|
|
20488
|
+
if (answer.ok) {
|
|
20489
|
+
handler(answer.value);
|
|
20490
|
+
}
|
|
20491
|
+
});
|
|
20492
|
+
this.send(frame, command, params, callbackId);
|
|
20493
|
+
return stopListening;
|
|
20494
|
+
}
|
|
20495
|
+
requireFrame() {
|
|
20496
|
+
if (!this.frame) {
|
|
20497
|
+
throw new BXRestFrameError('NOT_IN_FRAME', 'Bitrix24 frame parameters (DOMAIN, APP_SID) were not found in window.name or in the URL query: '
|
|
20498
|
+
+ 'the page is not opened as a Bitrix24 application.');
|
|
20499
|
+
}
|
|
20500
|
+
if (window.parent === window) {
|
|
20501
|
+
throw new BXRestFrameError('NOT_IN_FRAME', 'The page is not opened inside an iframe, so there is no Bitrix24 portal to send commands to.');
|
|
20502
|
+
}
|
|
20503
|
+
return this.frame;
|
|
20504
|
+
}
|
|
20505
|
+
/**
|
|
20506
|
+
* Формат — как у BX24.js. Обычно строка `команда:параметры:callbackId:APP_SID`
|
|
20507
|
+
* (портал разбирает её с конца, поэтому параметры и callbackId могут быть
|
|
20508
|
+
* пустыми); для команд с `:` в имени — объект.
|
|
20509
|
+
*/
|
|
20510
|
+
send(frame, command, params, callbackId) {
|
|
20511
|
+
const message = command.includes(':')
|
|
20512
|
+
? { method: command, params: params ? params : '', callback: callbackId, appSid: frame.appSid }
|
|
20513
|
+
: `${command}:${params ? JSON.stringify(params) : ''}:${callbackId}:${frame.appSid}`;
|
|
20514
|
+
window.parent.postMessage(message, frame.origin);
|
|
20515
|
+
}
|
|
20516
|
+
/** Ответ портала — строка `callbackId:JSON` (или `callbackId:`, если ответ пустой). */
|
|
20517
|
+
listen(frame, callbackId, onAnswer) {
|
|
20518
|
+
const portalWindow = window.parent;
|
|
20519
|
+
const onMessage = (event) => {
|
|
20520
|
+
// Ответ принимаем только от родительского окна и только с origin
|
|
20521
|
+
// портала — BX24.js проверяет лишь origin
|
|
20522
|
+
if (event.source !== portalWindow || event.origin !== frame.origin || typeof event.data !== 'string') {
|
|
20523
|
+
return;
|
|
20524
|
+
}
|
|
20525
|
+
const separator = event.data.indexOf(':');
|
|
20526
|
+
const id = separator < 0 ? event.data : event.data.slice(0, separator);
|
|
20527
|
+
if (id === callbackId) {
|
|
20528
|
+
onAnswer(separator < 0 ? '' : event.data.slice(separator + 1));
|
|
20529
|
+
}
|
|
20530
|
+
};
|
|
20531
|
+
window.addEventListener('message', onMessage);
|
|
20532
|
+
return () => window.removeEventListener('message', onMessage);
|
|
20533
|
+
}
|
|
20534
|
+
}
|
|
20535
|
+
function parsePayload(payload) {
|
|
20536
|
+
if (payload === '') {
|
|
20537
|
+
return { ok: true, value: undefined };
|
|
20538
|
+
}
|
|
20539
|
+
try {
|
|
20540
|
+
return { ok: true, value: JSON.parse(payload) };
|
|
20541
|
+
}
|
|
20542
|
+
catch {
|
|
20543
|
+
return { ok: false };
|
|
20544
|
+
}
|
|
20545
|
+
}
|
|
20546
|
+
function cancelled(command) {
|
|
20547
|
+
return new BXRestFrameError('CANCELLED', `Waiting for the Bitrix24 answer to ${command} was cancelled.`);
|
|
20548
|
+
}
|
|
20549
|
+
function readPortalFrame() {
|
|
20550
|
+
if (typeof window === 'undefined') {
|
|
20551
|
+
return undefined;
|
|
20552
|
+
}
|
|
20553
|
+
// BX24.js: window.name = 'DOMAIN|PROTOCOL|APP_SID'
|
|
20554
|
+
const [nameDomain, nameProtocol, nameAppSid] = (typeof window.name === 'string' ? window.name : '').split('|');
|
|
20555
|
+
const fromName = toPortalFrame(nameDomain, nameProtocol, nameAppSid);
|
|
20556
|
+
if (fromName) {
|
|
20557
|
+
return fromName;
|
|
20558
|
+
}
|
|
20559
|
+
try {
|
|
20560
|
+
const query = new URLSearchParams(window.location?.search ?? '');
|
|
20561
|
+
return toPortalFrame(query.get('DOMAIN'), query.get('PROTOCOL'), query.get('APP_SID'));
|
|
20562
|
+
}
|
|
20563
|
+
catch {
|
|
20564
|
+
return undefined;
|
|
20565
|
+
}
|
|
20566
|
+
}
|
|
20567
|
+
function toPortalFrame(rawDomain, rawProtocol, appSid) {
|
|
20568
|
+
// Как BX24.js: порт по умолчанию отрезается, PROTOCOL 1 — https
|
|
20569
|
+
const domain = (rawDomain ?? '').replace(/:(80|443)$/, '');
|
|
20570
|
+
if (!domain || !appSid) {
|
|
20571
|
+
return undefined;
|
|
20572
|
+
}
|
|
20573
|
+
const protocol = (Number.parseInt(rawProtocol ?? '', 10) || 0) ? 'https' : 'http';
|
|
20574
|
+
try {
|
|
20575
|
+
const url = new URL(`${protocol}://${domain}`);
|
|
20576
|
+
// В DOMAIN должен быть только хост (и порт) — никаких путей и логинов
|
|
20577
|
+
if (url.host !== domain.toLowerCase() || url.username || url.password) {
|
|
20578
|
+
return undefined;
|
|
20579
|
+
}
|
|
20580
|
+
return { domain, origin: url.origin, appSid };
|
|
20581
|
+
}
|
|
20582
|
+
catch {
|
|
20583
|
+
return undefined;
|
|
20584
|
+
}
|
|
20585
|
+
}
|
|
20586
|
+
function uniqId() {
|
|
20587
|
+
const bytes = new Uint8Array(16);
|
|
20588
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
20589
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
20590
|
+
}
|
|
20591
|
+
|
|
20592
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
20593
|
+
/**
|
|
20594
|
+
* Команды порталу Bitrix24 из iframe приложения — без подключения BX24.js.
|
|
20595
|
+
* Те же команды и те же имена, что у BX24.js, но вместо колбэков —
|
|
20596
|
+
* промисы. Протокол внутренний и недокументированный, см. `transport.ts`.
|
|
20597
|
+
*
|
|
20598
|
+
* Создавайте один экземпляр на страницу и при старте приложения: параметры
|
|
20599
|
+
* портала читаются в конструкторе (до того, как роутер перепишет адрес), а
|
|
20600
|
+
* `firstRun` портал сообщает только в первом ответе на `getInitData`.
|
|
20601
|
+
*
|
|
20602
|
+
* Токены класс не логирует и никуда не сохраняет: `getAuth()` и
|
|
20603
|
+
* `refreshAuth()` каждый раз спрашивают портал, а в `init()` токенов нет.
|
|
20604
|
+
*
|
|
20605
|
+
* @example
|
|
20606
|
+
* const frame = new BXRestFrame()
|
|
20607
|
+
* const { lang, placement } = await frame.init()
|
|
20608
|
+
* await frame.fitWindow()
|
|
20609
|
+
* const user = await frame.selectUser()
|
|
20610
|
+
*/
|
|
20611
|
+
class BXRestFrame {
|
|
20612
|
+
constructor(options = {}) {
|
|
20613
|
+
this.transport = new FrameTransport();
|
|
20614
|
+
// Открытый диалог каждого вида — новый вызов того же диалога отменяет прошлый
|
|
20615
|
+
this.dialogs = new Map();
|
|
20616
|
+
/** Мессенджер — как `BX24.im`. Портал на эти команды не отвечает. */
|
|
20617
|
+
this.im = {
|
|
20618
|
+
/** Позвонить сотруднику — `BX24.im.callTo()`. */
|
|
20619
|
+
callTo: async (userId, video) => {
|
|
20620
|
+
this.transport.post('imCallTo', { userId, video });
|
|
20621
|
+
},
|
|
20622
|
+
/** Позвонить на телефон — `BX24.im.phoneTo()`. */
|
|
20623
|
+
phoneTo: async (phone) => {
|
|
20624
|
+
this.transport.post('imPhoneTo', { phone });
|
|
20625
|
+
},
|
|
20626
|
+
/** Открыть чат — `BX24.im.openMessenger()`. */
|
|
20627
|
+
openMessenger: async (dialogId) => {
|
|
20628
|
+
this.transport.post('imOpenMessenger', { dialogId });
|
|
20629
|
+
},
|
|
20630
|
+
/** Открыть историю чата — `BX24.im.openHistory()`. */
|
|
20631
|
+
openHistory: async (dialogId) => {
|
|
20632
|
+
this.transport.post('imOpenHistory', { dialogId });
|
|
20633
|
+
},
|
|
20634
|
+
};
|
|
20635
|
+
/** Встраивание — как `BX24.placement`. */
|
|
20636
|
+
this.placement = {
|
|
20637
|
+
/** Код и параметры встраивания — `BX24.placement.info()`. */
|
|
20638
|
+
info: async () => {
|
|
20639
|
+
const { placement, placementOptions } = await this.init();
|
|
20640
|
+
return { placement, options: placementOptions };
|
|
20641
|
+
},
|
|
20642
|
+
/** Команды и события текущего встраивания — `BX24.placement.getInterface()`. */
|
|
20643
|
+
getInterface: async () => {
|
|
20644
|
+
return await this.transport.request('getInterface', null, { timeout: this.timeout });
|
|
20645
|
+
},
|
|
20646
|
+
/**
|
|
20647
|
+
* Команда встраивания — `BX24.placement.call()`. Набор команд зависит от
|
|
20648
|
+
* встраивания (см. `getInterface()`). Не на все команды портал отвечает:
|
|
20649
|
+
* без ответа за `timeout` промис завершается с `undefined`.
|
|
20650
|
+
*/
|
|
20651
|
+
call: async (command, params, options = {}) => {
|
|
20652
|
+
return this.transport.request(command, params, {
|
|
20653
|
+
timeout: options.timeout ?? this.timeout,
|
|
20654
|
+
onTimeout: 'resolve',
|
|
20655
|
+
});
|
|
20656
|
+
},
|
|
20657
|
+
/**
|
|
20658
|
+
* Подписка на событие встраивания — `BX24.placement.bindEvent()`.
|
|
20659
|
+
* `handler` вызывается при каждом событии. Возвращает функцию отписки:
|
|
20660
|
+
* она только перестаёт вызывать `handler` — отменить подписку на
|
|
20661
|
+
* стороне портала протокол не позволяет. Вне iframe бросает
|
|
20662
|
+
* `BXRestFrameError` с кодом `NOT_IN_FRAME`.
|
|
20663
|
+
*/
|
|
20664
|
+
bindEvent: (event, handler) => {
|
|
20665
|
+
return this.transport.subscribe('placementBindEvent', { event }, handler);
|
|
20666
|
+
},
|
|
20667
|
+
};
|
|
20668
|
+
/** Настройки приложения для текущего пользователя — как `BX24.userOption`. */
|
|
20669
|
+
this.userOption = {
|
|
20670
|
+
get: async (name) => (await this.init()).userOptions[name],
|
|
20671
|
+
set: async (name, value) => {
|
|
20672
|
+
const init = await this.init();
|
|
20673
|
+
init.userOptions[name] = value;
|
|
20674
|
+
await this.ack('setUserOption', { name, value });
|
|
20675
|
+
},
|
|
20676
|
+
};
|
|
20677
|
+
/** Настройки приложения для всего портала — как `BX24.appOption`. */
|
|
20678
|
+
this.appOption = {
|
|
20679
|
+
get: async (name) => (await this.init()).appOptions[name],
|
|
20680
|
+
/**
|
|
20681
|
+
* Менять их может только администратор портала: остальным — ошибка
|
|
20682
|
+
* `ACCESS_DENIED` (BX24.js в этом случае молча ничего не делает).
|
|
20683
|
+
* Возвращает настройки приложения после сохранения.
|
|
20684
|
+
*/
|
|
20685
|
+
set: async (name, value) => {
|
|
20686
|
+
const init = await this.init();
|
|
20687
|
+
if (!init.isAdmin) {
|
|
20688
|
+
throw new BXRestFrameError('ACCESS_DENIED', 'Only a Bitrix24 administrator can change application options.');
|
|
20689
|
+
}
|
|
20690
|
+
init.appOptions[name] = value;
|
|
20691
|
+
return await this.transport.request('setAppOption', { name, value }, { timeout: this.timeout });
|
|
20692
|
+
},
|
|
20693
|
+
};
|
|
20694
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
20695
|
+
}
|
|
20696
|
+
/**
|
|
20697
|
+
* Данные запуска приложения — аналог `BX24.init()`: язык, домен, права,
|
|
20698
|
+
* встраивание, настройки, признаки установки. Запрашиваются у портала
|
|
20699
|
+
* один раз, дальше берутся из памяти. Токенов здесь нет — см. `getAuth()`.
|
|
20700
|
+
*/
|
|
20701
|
+
init() {
|
|
20702
|
+
if (this.initValue) {
|
|
20703
|
+
return Promise.resolve(this.initValue);
|
|
20704
|
+
}
|
|
20705
|
+
if (!this.initRequest) {
|
|
20706
|
+
this.initRequest = this.getInitData()
|
|
20707
|
+
.then(() => this.initValue)
|
|
20708
|
+
.finally(() => {
|
|
20709
|
+
this.initRequest = undefined;
|
|
20710
|
+
});
|
|
20711
|
+
}
|
|
20712
|
+
return this.initRequest;
|
|
20713
|
+
}
|
|
20714
|
+
/**
|
|
20715
|
+
* Текущая авторизация приложения — аналог `BX24.getAuth()`. Каждый вызов —
|
|
20716
|
+
* новый запрос `getInitData` к порталу.
|
|
20717
|
+
*
|
|
20718
|
+
* `expires_in` портал считает от срока жизни токена в момент выдачи, а не
|
|
20719
|
+
* от остатка: если страница открыта давно, значение будет завышено (так
|
|
20720
|
+
* же и у BX24.js). Для свежего токена используйте `refreshAuth()`.
|
|
20721
|
+
*/
|
|
20722
|
+
async getAuth() {
|
|
20723
|
+
const data = await this.getInitData();
|
|
20724
|
+
return this.toAuth('getInitData', data, String(data['MEMBER_ID'] || ''));
|
|
20725
|
+
}
|
|
20726
|
+
/**
|
|
20727
|
+
* Новая авторизация приложения — аналог `BX24.refreshAuth()`: портал
|
|
20728
|
+
* выпускает новую пару `access_token`/`refresh_token`. Если портал не может
|
|
20729
|
+
* выдать токен, он показывает `alert` «Unable to get new token!» и не
|
|
20730
|
+
* отвечает — тогда ошибка `TIMEOUT`.
|
|
20731
|
+
*/
|
|
20732
|
+
async refreshAuth() {
|
|
20733
|
+
// member_id приходит только в getInitData — refreshAuth его не возвращает
|
|
20734
|
+
const { memberId } = await this.init();
|
|
20735
|
+
const data = asObject('refreshAuth', await this.transport.request('refreshAuth', {}, { timeout: this.timeout }));
|
|
20736
|
+
return this.toAuth('refreshAuth', data, memberId);
|
|
20737
|
+
}
|
|
20738
|
+
/**
|
|
20739
|
+
* Завершить установку — аналог `BX24.installFinish()`: портал отмечает
|
|
20740
|
+
* приложение установленным и перезагружает страницу. Делает что-то только
|
|
20741
|
+
* на странице установщика (`init().install`), иначе — ничего, как и BX24.js.
|
|
20742
|
+
*/
|
|
20743
|
+
async installFinish() {
|
|
20744
|
+
if ((await this.init()).install) {
|
|
20745
|
+
this.transport.post('setInstallFinish', {});
|
|
20746
|
+
}
|
|
20747
|
+
}
|
|
20748
|
+
/**
|
|
20749
|
+
* Отметить, что первый запуск у этого пользователя пройден, — то, что
|
|
20750
|
+
* BX24.js делает после последнего обработчика `BX24.install()`. Без этого
|
|
20751
|
+
* `init().firstRun` останется `true` и при следующих запусках. Делает
|
|
20752
|
+
* что-то только при первом запуске.
|
|
20753
|
+
*/
|
|
20754
|
+
async firstRunFinish() {
|
|
20755
|
+
const init = await this.init();
|
|
20756
|
+
if (init.firstRun) {
|
|
20757
|
+
await this.ack('setInstall', { install: true });
|
|
20758
|
+
init.firstRun = false;
|
|
20759
|
+
}
|
|
20760
|
+
}
|
|
20761
|
+
/** Задать размер фрейма — `BX24.resizeWindow()`. Возвращает размер, который выставил портал. */
|
|
20762
|
+
async resizeWindow(width, height) {
|
|
20763
|
+
const w = Number.parseInt(String(width), 10);
|
|
20764
|
+
const h = Number.parseInt(String(height), 10);
|
|
20765
|
+
if (!(w > 0 && h > 0)) {
|
|
20766
|
+
throw new TypeError('resizeWindow: width and height must be positive numbers.');
|
|
20767
|
+
}
|
|
20768
|
+
return await this.ack('resizeWindow', { width: w, height: h });
|
|
20769
|
+
}
|
|
20770
|
+
/** Подогнать высоту фрейма под содержимое — `BX24.fitWindow()`. */
|
|
20771
|
+
async fitWindow() {
|
|
20772
|
+
const root = document.documentElement;
|
|
20773
|
+
const height = Math.max(root.scrollHeight, root.offsetHeight);
|
|
20774
|
+
return await this.ack('resizeWindow', { width: '100%', height });
|
|
20775
|
+
}
|
|
20776
|
+
/**
|
|
20777
|
+
* Перезагрузить страницу портала с приложением — `BX24.reloadWindow()`.
|
|
20778
|
+
* Портал на эту команду не отвечает.
|
|
20779
|
+
*/
|
|
20780
|
+
async reloadWindow() {
|
|
20781
|
+
this.transport.post('reloadWindow', {});
|
|
20782
|
+
}
|
|
20783
|
+
/** Заголовок страницы портала — `BX24.setTitle()`. */
|
|
20784
|
+
async setTitle(title) {
|
|
20785
|
+
return await this.ack('setTitle', { title: String(title) });
|
|
20786
|
+
}
|
|
20787
|
+
/** Прокрутить страницу портала — `BX24.scrollParentWindow()`. */
|
|
20788
|
+
async scrollParentWindow(scroll) {
|
|
20789
|
+
const value = Number.parseInt(String(scroll), 10);
|
|
20790
|
+
if (Number.isNaN(value)) {
|
|
20791
|
+
throw new TypeError('scrollParentWindow: scroll must be a number.');
|
|
20792
|
+
}
|
|
20793
|
+
return await this.ack('setScroll', { scroll: value });
|
|
20794
|
+
}
|
|
20795
|
+
/**
|
|
20796
|
+
* Диалог выбора одного сотрудника — `BX24.selectUser()`. Ждёт выбора без
|
|
20797
|
+
* ограничения по времени: если пользователь закрыл диалог, портал не
|
|
20798
|
+
* отвечает вовсе. Отменить ожидание — `signal` или новый вызов
|
|
20799
|
+
* `selectUser()`/`selectUsers()` (ошибка `CANCELLED`).
|
|
20800
|
+
*/
|
|
20801
|
+
selectUser(options = {}) {
|
|
20802
|
+
return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: false }, options.signal);
|
|
20803
|
+
}
|
|
20804
|
+
/** Диалог выбора нескольких сотрудников — `BX24.selectUsers()`. Ожидание — как у `selectUser()`. */
|
|
20805
|
+
selectUsers(options = {}) {
|
|
20806
|
+
return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: true }, options.signal);
|
|
20807
|
+
}
|
|
20808
|
+
/**
|
|
20809
|
+
* Диалог выбора прав доступа — `BX24.selectAccess()`. `value` — коды,
|
|
20810
|
+
* которые нельзя выбрать. Ожидание — как у `selectUser()`.
|
|
20811
|
+
*/
|
|
20812
|
+
selectAccess(options = {}) {
|
|
20813
|
+
return this.dialog('access', 'selectAccess', { value: options.value ?? [] }, options.signal);
|
|
20814
|
+
}
|
|
20815
|
+
/** Диалог выбора объектов CRM — `BX24.selectCRM()`. Ожидание — как у `selectUser()`. */
|
|
20816
|
+
selectCRM(options = {}) {
|
|
20817
|
+
const { entityType, multiple, value, signal } = options;
|
|
20818
|
+
return this.dialog('crm', 'selectCRM', { entityType, multiple, value }, signal);
|
|
20819
|
+
}
|
|
20820
|
+
/**
|
|
20821
|
+
* Открыть приложение в слайдере — `BX24.openApplication()`. Промис
|
|
20822
|
+
* завершается, когда слайдер закрыли. `settings` — настройки слайдера
|
|
20823
|
+
* (уходят как `bx24_<ключ>`, как в BX24.js).
|
|
20824
|
+
*/
|
|
20825
|
+
async openApplication(params = {}, settings = {}, options = {}) {
|
|
20826
|
+
const message = { ...params };
|
|
20827
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
20828
|
+
message[`bx24_${key}`] = value;
|
|
20829
|
+
}
|
|
20830
|
+
await this.transport.request('openApplication', message, { timeout: false, signal: options.signal });
|
|
20831
|
+
}
|
|
20832
|
+
/**
|
|
20833
|
+
* Открыть страницу портала в слайдере — `BX24.openPath()`. Промис
|
|
20834
|
+
* завершается, когда слайдер закрыли (`{ result: 'close' }`), или сразу,
|
|
20835
|
+
* если путь не открылся (`{ result: 'error', errorCode }`).
|
|
20836
|
+
*/
|
|
20837
|
+
async openPath(path, options = {}) {
|
|
20838
|
+
return await this.transport.request('openPath', { path }, { timeout: false, signal: options.signal });
|
|
20839
|
+
}
|
|
20840
|
+
/**
|
|
20841
|
+
* Закрыть слайдер с приложением — `BX24.closeApplication()`. Если
|
|
20842
|
+
* приложение открыто не в слайдере, ничего не происходит. Узнать о
|
|
20843
|
+
* закрытии может открывший слайдер — через `openApplication()`.
|
|
20844
|
+
*/
|
|
20845
|
+
async closeApplication() {
|
|
20846
|
+
this.transport.post('closeApplication', null);
|
|
20847
|
+
}
|
|
20848
|
+
/**
|
|
20849
|
+
* Команда, на которую портал отвечает подтверждением. В некоторых
|
|
20850
|
+
* встраиваниях портал на такие команды не отвечает (так считает и
|
|
20851
|
+
* b24jssdk) — тогда промис завершается с `undefined`, а не ошибкой.
|
|
20852
|
+
*/
|
|
20853
|
+
ack(command, params) {
|
|
20854
|
+
return this.transport.request(command, params, { timeout: this.timeout, onTimeout: 'resolve' });
|
|
20855
|
+
}
|
|
20856
|
+
/**
|
|
20857
|
+
* Диалоги ждут пользователя без таймаута. Портал держит один обработчик
|
|
20858
|
+
* на диалог (у выбора сотрудников он общий для `selectUser` и
|
|
20859
|
+
* `selectUsers`) и на прошлый вызов уже не ответит — поэтому новый вызов
|
|
20860
|
+
* отменяет ожидание прошлого.
|
|
20861
|
+
*/
|
|
20862
|
+
dialog(kind, command, params, signal) {
|
|
20863
|
+
this.dialogs.get(kind)?.abort();
|
|
20864
|
+
const controller = new AbortController();
|
|
20865
|
+
this.dialogs.set(kind, controller);
|
|
20866
|
+
const onAbort = () => controller.abort();
|
|
20867
|
+
if (signal?.aborted) {
|
|
20868
|
+
controller.abort();
|
|
20869
|
+
}
|
|
20870
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
20871
|
+
return this.transport.request(command, params, { timeout: false, signal: controller.signal })
|
|
20872
|
+
.finally(() => {
|
|
20873
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20874
|
+
if (this.dialogs.get(kind) === controller) {
|
|
20875
|
+
this.dialogs.delete(kind);
|
|
20876
|
+
}
|
|
20877
|
+
});
|
|
20878
|
+
}
|
|
20879
|
+
async getInitData() {
|
|
20880
|
+
const data = asObject('getInitData', await this.transport.request('getInitData', null, { timeout: this.timeout }));
|
|
20881
|
+
// firstRun портал отдаёт только в первом ответе за загрузку страницы —
|
|
20882
|
+
// поэтому запоминаем данные первого ответа, кто бы его ни запросил
|
|
20883
|
+
if (!this.initValue) {
|
|
20884
|
+
this.initValue = this.toInit(data);
|
|
20885
|
+
}
|
|
20886
|
+
return data;
|
|
20887
|
+
}
|
|
20888
|
+
toInit(data) {
|
|
20889
|
+
return {
|
|
20890
|
+
// Как BX24.js: домен — из параметров фрейма, DOMAIN ответа — запасной
|
|
20891
|
+
domain: this.transport.domain || String(data['DOMAIN'] || ''),
|
|
20892
|
+
lang: String(data['LANG'] || ''),
|
|
20893
|
+
path: String(data['PATH'] || ''),
|
|
20894
|
+
https: Number.parseInt(String(data['PROTOCOL']), 10) === 1,
|
|
20895
|
+
memberId: String(data['MEMBER_ID'] || ''),
|
|
20896
|
+
isAdmin: !!data['IS_ADMIN'],
|
|
20897
|
+
install: !!data['INSTALL'],
|
|
20898
|
+
firstRun: !!data['FIRST_RUN'],
|
|
20899
|
+
placement: String(data['PLACEMENT'] || 'DEFAULT'),
|
|
20900
|
+
placementOptions: data['PLACEMENT_OPTIONS'],
|
|
20901
|
+
userOptions: toRecord(data['USER_OPTIONS']),
|
|
20902
|
+
appOptions: toRecord(data['APP_OPTIONS']),
|
|
20903
|
+
};
|
|
20904
|
+
}
|
|
20905
|
+
toAuth(command, data, memberId) {
|
|
20906
|
+
const { AUTH_ID, REFRESH_ID, AUTH_EXPIRES } = data;
|
|
20907
|
+
if (!AUTH_ID || typeof AUTH_ID !== 'string') {
|
|
20908
|
+
throw new BXRestFrameError('NO_AUTH', `Bitrix24 answered ${command} without an access token.`);
|
|
20909
|
+
}
|
|
20910
|
+
// AUTH_EXPIRES — срок жизни токена в секундах. BX24.js переводит его в
|
|
20911
|
+
// момент истечения в миллисекундах — так же, как здесь.
|
|
20912
|
+
const expiresIn = Number.parseInt(String(AUTH_EXPIRES), 10);
|
|
20913
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
20914
|
+
throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} without a usable AUTH_EXPIRES.`);
|
|
20915
|
+
}
|
|
20916
|
+
return {
|
|
20917
|
+
access_token: AUTH_ID,
|
|
20918
|
+
refresh_token: typeof REFRESH_ID === 'string' ? REFRESH_ID : '',
|
|
20919
|
+
expires_in: Date.now() + expiresIn * 1000,
|
|
20920
|
+
domain: this.transport.domain,
|
|
20921
|
+
member_id: memberId,
|
|
20922
|
+
};
|
|
20923
|
+
}
|
|
20924
|
+
}
|
|
20925
|
+
function asObject(command, data) {
|
|
20926
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
20927
|
+
throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not an object.`);
|
|
20928
|
+
}
|
|
20929
|
+
return data;
|
|
20930
|
+
}
|
|
20931
|
+
// Пустые настройки PHP отдаёт массивом `[]`
|
|
20932
|
+
function toRecord(value) {
|
|
20933
|
+
return (value && typeof value === 'object' && !Array.isArray(value))
|
|
20934
|
+
? { ...value }
|
|
20935
|
+
: {};
|
|
20936
|
+
}
|
|
20937
|
+
|
|
20234
20938
|
/**
|
|
20235
20939
|
* Public API Surface of bx-rest
|
|
20236
20940
|
*/
|
|
@@ -20239,5 +20943,5 @@ class BXRestMap {
|
|
|
20239
20943
|
* Generated bundle index. Do not edit.
|
|
20240
20944
|
*/
|
|
20241
20945
|
|
|
20242
|
-
export { base as BXBaseServices, BXRestApiError, BXRestMap, BXRestMapResult, BXRestNavvy, HttpBXServices as BXRestRequest, BXRestSettings, HttpNavvyServices, Navvy, SessionKeyError, SessionKeyServices, instanceOfiBXRestAnswerSuccess as isBXRestAnswerSuccess, partNameMethods$1 as partNameMethods };
|
|
20946
|
+
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 };
|
|
20243
20947
|
//# sourceMappingURL=bx-rest.mjs.map
|