bx-rest 1.1.0 → 1.3.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.
@@ -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) {
@@ -970,21 +977,26 @@ class HttpBXServices extends HttpServices {
970
977
  *
971
978
  * @param name
972
979
  * @param params
980
+ * @param restApi
981
+ * @param headers заголовки только для этого запроса, поверх `BXRestSettings.headers`
973
982
  */
974
- post(name, params = {}, restApi = false) {
983
+ post(name, params = {}, restApi = false, headers) {
975
984
  const method = this.getNameMethod(name);
976
- return this.httpPost(restApi ? `api/${method}` : method, params);
985
+ return this.httpPost(restApi ? `api/${method}` : method, params, undefined, headers);
977
986
  }
978
987
  /**
979
988
  * Второстепенный метод для запросов. По возможности лучше не использовать
980
989
  *
981
990
  * @param name
982
991
  * @param params
992
+ * @param headers заголовки только для этого запроса, поверх `BXRestSettings.headers`
983
993
  */
984
- get(name, params = {}) {
985
- return this.httpGet(this.getNameMethod(name), params);
994
+ get(name, params = {}, headers) {
995
+ return this.httpGet(this.getNameMethod(name), params, undefined, headers);
986
996
  }
987
- httpPost(url, params = {}) {
997
+ // settings — только для совместимости сигнатуры с HttpServices: здесь тело
998
+ // уходит JSON-ом/urlencoded, и пересчёт дат из iHttpParamSettings не нужен.
999
+ httpPost(url, params = {}, settings, headers) {
988
1000
  // defer() откладывает всё содержимое (в том числе чтение BXRestSettings
989
1001
  // через this.session.*) до реальной подписки — иначе даже с "живым"
990
1002
  // геттером в SessionKeyServices настройки читались бы в момент вызова
@@ -1012,19 +1024,17 @@ class HttpBXServices extends HttpServices {
1012
1024
  // Преобразуем параметры в формат x-www-form-urlencoded
1013
1025
  const bodyString = this.serializeBitrixParams(paramsClone);
1014
1026
  return from(this.axiosInstance.post(fullUrl, bodyString, {
1015
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1027
+ headers: this.buildHeaders(headers, { 'Content-Type': 'application/x-www-form-urlencoded' }),
1016
1028
  }).then((response) => response.data));
1017
1029
  }
1018
1030
  return from(this.axiosInstance.post(fullUrl, paramsClone, {
1019
1031
  withCredentials: this.session.getWithCredentials(),
1020
- headers: {
1021
- 'Content-Type': 'application/json',
1022
- }
1032
+ headers: this.buildHeaders(headers, { 'Content-Type': 'application/json' }),
1023
1033
  }).then((response) => response.data));
1024
1034
  }));
1025
1035
  });
1026
1036
  }
1027
- httpGet(url, params = {}) {
1037
+ httpGet(url, params = {}, settings, headers) {
1028
1038
  // Та же причина, что и у httpPost выше: откладываем чтение BXRestSettings
1029
1039
  // до подписки, чтобы `get()` вёл себя так же "лениво", как и `post()`
1030
1040
  // (симметрия, которой не было до этого — GET резолвил настройки в момент
@@ -1046,10 +1056,21 @@ class HttpBXServices extends HttpServices {
1046
1056
  const fullUrl = prepareBaseAddress(baseUrl) + url;
1047
1057
  const queryString = new URLSearchParams(paramsClone).toString();
1048
1058
  const finalUrl = `${fullUrl}?${queryString}`;
1049
- return from(this.axiosInstance.get(finalUrl).then((response) => response.data));
1059
+ return from(this.axiosInstance.get(finalUrl, {
1060
+ headers: this.buildHeaders(headers),
1061
+ }).then((response) => response.data));
1050
1062
  }));
1051
1063
  });
1052
1064
  }
1065
+ /**
1066
+ * Заголовки запроса: `BXRestSettings.headers`, поверх них — заголовки
1067
+ * конкретного запроса, поверх всего — `required`. В `required` идёт
1068
+ * `Content-Type`: от него зависит, как сервер разберёт тело, поэтому
1069
+ * пользовательские заголовки его не перебивают.
1070
+ */
1071
+ buildHeaders(headers, required = {}) {
1072
+ return { ...this.session.getHeaders(), ...headers, ...required };
1073
+ }
1053
1074
  serializeBitrixParams(params) {
1054
1075
  return serializeBitrixParams(params);
1055
1076
  }
@@ -1059,20 +1080,75 @@ class HttpBXServices extends HttpServices {
1059
1080
  }
1060
1081
 
1061
1082
  /**
1062
- * Thrown by `.res()` when called with `{ throwOnApiError: true }` and the
1063
- * Bitrix REST call resolves as a Bitrix-level API error, e.g.
1064
- * `INSUFFICIENT_SCOPE` or `expired_token`.
1083
+ * A Bitrix-level API error, e.g. `INSUFFICIENT_SCOPE` or `expired_token`.
1065
1084
  *
1066
- * By default `.res()` resolves such responses as `undefined` instead of
1067
- * throwing — see the "Error handling" section of the README.
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.
1068
1090
  */
1069
1091
  class BXRestApiError extends Error {
1070
- constructor(error, error_description) {
1071
- super(error_description || error || 'Bitrix API error');
1092
+ constructor(error, error_description, details) {
1093
+ super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
1072
1094
  this.error = error;
1073
1095
  this.error_description = error_description;
1074
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
+ };
1075
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
+ });
1076
1152
  }
1077
1153
 
1078
1154
  class NavvyPagBase {
@@ -1084,8 +1160,21 @@ class NavvyPagBase {
1084
1160
  this.pageSize = 50; // стандартный размер страницы
1085
1161
  this.http = new HttpBXServices();
1086
1162
  }
1163
+ /**
1164
+ * HTTP-заголовки только для этого запроса — поверх `BXRestSettings.headers`
1165
+ * (при совпадении имени побеждает значение отсюда). Повторные вызовы
1166
+ * дополняют уже заданные заголовки. Действует на `.res()`, `.resVanilla()`,
1167
+ * `.mapForVanilla()` и на все batch-запросы `.resAll()`.
1168
+ *
1169
+ * @example
1170
+ * bx.crm.deal.list({ ... }).withHeaders({ 'X-Request-Source': 'report' }).resAll()
1171
+ */
1172
+ withHeaders(headers) {
1173
+ this.requestHeaders = { ...this.requestHeaders, ...headers };
1174
+ return this;
1175
+ }
1087
1176
  resVanilla() {
1088
- return this.http.post(this.url, this.param, this.restApi);
1177
+ return this.http.post(this.url, this.param, this.restApi, this.requestHeaders);
1089
1178
  }
1090
1179
  mapForVanilla() {
1091
1180
  return this.resVanilla()
@@ -1104,9 +1193,11 @@ class NavvyPagBase {
1104
1193
  : v));
1105
1194
  }
1106
1195
  mapAndError(request, options) {
1196
+ const method = this.http.getNameMethod(this.url);
1107
1197
  return request.pipe(map(v => {
1108
1198
  if (options?.throwOnApiError && v && !instanceOfiBXRestAnswerSuccess(v)) {
1109
- throw new BXRestApiError(v.error, v.error_description);
1199
+ const parsed = parseBXRestErrorBody(v);
1200
+ throw new BXRestApiError(parsed ? parsed.error : v.error, parsed ? parsed.error_description : v.error_description, { method });
1110
1201
  }
1111
1202
  return BXRestMapResult(v);
1112
1203
  }), catchError((err) => {
@@ -1114,6 +1205,13 @@ class NavvyPagBase {
1114
1205
  if (err instanceof BXRestApiError) {
1115
1206
  throw err;
1116
1207
  }
1208
+ // Большинство ошибок Bitrix приходит со статусом не 2xx, и axios
1209
+ // отклоняет такой ответ — достаём из него код и описание ошибки,
1210
+ // а не отдаём безликое "AxiosError: Request failed with status code 400".
1211
+ const apiError = bxRestApiErrorFromHttp(err, method);
1212
+ if (apiError) {
1213
+ throw apiError;
1214
+ }
1117
1215
  throw new Error((err.error) ? err.error : err);
1118
1216
  }));
1119
1217
  }
@@ -3042,8 +3140,8 @@ class BXRest {
3042
3140
  profile() {
3043
3141
  return this.http.post(['profile']);
3044
3142
  }
3045
- batch(param) {
3046
- return this.http.post(['batch'], param);
3143
+ batch(param, headers) {
3144
+ return this.http.post(['batch'], param, false, headers);
3047
3145
  }
3048
3146
  }
3049
3147
 
@@ -3095,7 +3193,7 @@ class NavvyAlterPagNav extends NavvyPagBase {
3095
3193
  return this.http.getNameMethod(this.url)
3096
3194
  + '?' + serializeBitrixParams(param);
3097
3195
  })
3098
- });
3196
+ }, this.requestHeaders);
3099
3197
  });
3100
3198
  return from(batchRequests).pipe(concatMap(request => request), // Выполняет запросы по очереди
3101
3199
  toArray(), // Собирает все результаты в массив
@@ -3151,7 +3249,7 @@ class NavvyPagNavWithUselessKey extends NavvyPagBase {
3151
3249
  return this.http.getNameMethod(this.url)
3152
3250
  + '?' + serializeBitrixParams(param);
3153
3251
  })
3154
- }).pipe(map(v => {
3252
+ }, this.requestHeaders).pipe(map(v => {
3155
3253
  if (v && instanceOfiBXRestAnswerSuccess(v) && v.result && v.result.result && items.result) {
3156
3254
  return Object.assign({}, items.result, v.result.result);
3157
3255
  }
@@ -3237,7 +3335,7 @@ class NavvyPagNavTasks extends NavvyPagBase {
3237
3335
  return this.http.getNameMethod(this.url)
3238
3336
  + '?' + serializeBitrixParams(param);
3239
3337
  })
3240
- });
3338
+ }, this.requestHeaders);
3241
3339
  });
3242
3340
  return from(batchRequests).pipe(concatMap(request => request), // Выполняет запросы по очереди
3243
3341
  toArray(), // Собирает все результаты в массив
@@ -3301,7 +3399,7 @@ class NavvyPag extends NavvyPagBase {
3301
3399
  return this.http.getNameMethod(this.url)
3302
3400
  + '?' + serializeBitrixParams(param);
3303
3401
  })
3304
- }).pipe();
3402
+ }, this.requestHeaders).pipe();
3305
3403
  });
3306
3404
  return from(batchRequests).pipe(concatMap(request => request), // Выполняет запросы по очереди
3307
3405
  toArray(), // Собирает все результаты в массив
@@ -3371,7 +3469,7 @@ class NavvyPagNavResultKey extends NavvyPagBase {
3371
3469
  return this.http.getNameMethod(this.url)
3372
3470
  + '?' + serializeBitrixParams(param);
3373
3471
  })
3374
- });
3472
+ }, this.requestHeaders);
3375
3473
  });
3376
3474
  return from(batchRequests).pipe(concatMap(request => request), toArray(), map(results => {
3377
3475
  const firstPage = items.result?.[this.key];
@@ -3427,7 +3525,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
3427
3525
  return bxRest.batch({
3428
3526
  halt: 1,
3429
3527
  cmd: [this.http.getNameMethod(this.url) + '?' + serializeBitrixParams(param)]
3430
- });
3528
+ }, this.requestHeaders);
3431
3529
  });
3432
3530
  return from(commands).pipe(concatMap(request => request), toArray(), mergeMap(results => {
3433
3531
  const merged = { ...(items.result?.[this.key] ?? {}) };