wmakeev/moysklad

View on GitHub
README.md

Summary

Maintainability
Test Coverage
![moysklad](https://wmakeev-public-files.s3-eu-west-1.amazonaws.com/images/logos/logoMS500x350.png)

# moysklad

[![npm](https://img.shields.io/npm/v/moysklad.svg?cacheSeconds=1800&style=flat-square)](https://www.npmjs.com/package/moysklad)
[![Travis](https://img.shields.io/travis/wmakeev/moysklad.svg?cacheSeconds=1800&style=flat-square)](https://travis-ci.org/wmakeev/moysklad)
![Code Climate coverage](https://img.shields.io/codeclimate/coverage/wmakeev/moysklad?style=flat-square)
![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/wmakeev/moysklad.svg?cacheSeconds=1800&style=flat-square)
![Code Climate tech-debt](https://img.shields.io/codeclimate/tech-debt/wmakeev/moysklad.svg?cacheSeconds=1800&style=flat-square)

> Библиотека для взаимодействия с [JSON API сервиса МойСклад](https://dev.moysklad.ru/) для node.js и браузера.

> **ВНИМАНИЕ!** Библиотека находится в стадии разработки и становления. API может меняться. Перед обновлением минорной версии смотрите [историю изменений](https://github.com/wmakeev/moysklad/blob/master/CHANGELOG.md).

## Содержание

<!-- TOC depthfrom:2 -->

- [Содержание](#%D1%81%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D0%BD%D0%B8%D0%B5)
- [Особенности](#%D0%BE%D1%81%D0%BE%D0%B1%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8)
- [Установка](#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0)
- [Использование](#%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)
- [Параметры инициализации](#%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B-%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8)
- [Аутентификация](#%D0%B0%D1%83%D1%82%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F)
- [Статические методы](#%D1%81%D1%82%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5-%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D1%8B)
  - [getTimeString](#gettimestring)
  - [parseTimeString](#parsetimestring)
  - [parseUrl статический метод](#parseurl-%D1%81%D1%82%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9-%D0%BC%D0%B5%D1%82%D0%BE%D0%B4)
  - [buildFilter](#buildfilter)
  - [buildQuery](#buildquery)
- [Методы экземпляра](#%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D1%8B-%D1%8D%D0%BA%D0%B7%D0%B5%D0%BC%D0%BF%D0%BB%D1%8F%D1%80%D0%B0)
  - [GET](#get)
  - [POST](#post)
  - [PUT](#put)
  - [DELETE](#delete)
  - [getOptions](#getoptions)
  - [buildUrl](#buildurl)
  - [parseUrl](#parseurl)
  - [fetchUrl](#fetchurl)
  - [Основные аргументы](#%D0%BE%D1%81%D0%BD%D0%BE%D0%B2%D0%BD%D1%8B%D0%B5-%D0%B0%D1%80%D0%B3%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B)
    - [path](#path)
    - [query](#query)
      - [querystring](#querystring)
      - [filter](#filter)
      - [order](#order)
      - [expand и limit](#expand-%D0%B8-limit)
    - [options параметры запроса](#options-%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B-%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%B0)
- [События](#%D1%81%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D1%8F)
- [Работа с ошибками](#%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0-%D1%81-%D0%BE%D1%88%D0%B8%D0%B1%D0%BA%D0%B0%D0%BC%D0%B8)
  - [MoyskladError](#moyskladerror)
  - [MoyskladRequestError](#moyskladrequesterror)
  - [MoyskladApiError](#moyskladapierror)
  - [MoyskladCollectionError](#moyskladcollectionerror)
  - [MoyskladUnexpectedRedirectError](#moyskladunexpectedredirecterror)
- [История изменений](#%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D1%8F-%D0%B8%D0%B7%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9)
- [Планы развития](#%D0%BF%D0%BB%D0%B0%D0%BD%D1%8B-%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D1%8F)
- [TODO](#todo)

<!-- /TOC -->

## Особенности

Библиотека представляет максимально простой и прозрачный интерфейс к существующим методам [API МойСклад](https://api.moysklad.ru/api/remap/1.2/doc), не абстрагирует разработчика от API и не выполняет никаких внутренних преобразований отправляемых и получаемых данных.

Основная задача библиотеки - упростить ряд рутинных задач:

- формирование строки запроса (передача параметров, заголовков и фильтрация)
- обработка ошибок
- явное преобразование даты в формат МойСклад и обратно в `Date`
- базовые TypeScript тайпинги для подсказок по API библиотеки

Важно отметить, что библиотека не поможет вам разобраться с API МойСклад, но лишь упростит работу с ним.

## Установка

> Поддерживаются версии Node.js >=14

```
$ npm install moysklad
```

Для Node.js до 18 версии, дополнительно нужно установить библиотеку для
[Fetch API](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) и явно указать модуль с соответствующим интерфейсом при создании экземпляра библиотеки

```
$ npm install undici
```

[undici.fetch](https://github.com/nodejs/undici#undicifetchinput-init-promise)

```js
const { fetch } = require('undici')

const Moysklad = require('moysklad')

const moysklad = Moysklad({ fetch })
```

Для работы с библиотекой в браузере или для Node.js c 18 версии, установка дополнительного модуля не требуется.

## Использование

```js
const Moysklad = require('moysklad')

// Для инициализации экземпляра библиотеки указывать ключевое слово new не нужно
const ms = Moysklad({ login, password })

ms.GET('entity/customerorder', {
  filter: {
    applicable: true,
    state: {
      name: 'Отгружен'
    },
    sum: { $gt: 1000000, $lt: 2000000 }
  },
  limit: 10,
  order: 'moment,desc',
  expand: 'agent'
}).then(({ meta, rows }) => {
  console.log(
    `Последние ${meta.limit} из ${meta.size} проведенных заказов ` +
      `в статусе "Отгружен" на сумму от 10000 до 20000 руб`
  )

  // Выводим имя заказа, имя контрагента и сумму заказа для всех позиций
  rows.forEach(row => {
    console.log(`${row.name} ${row.agent.name} ${row.sum / 100}`)
  })
})
```

> С другими примерами использования можно ознакомиться в папке [examples](https://github.com/wmakeev/moysklad/tree/master/examples)

## Параметры инициализации

Все параметры опциональные (имеют значения по умолчанию)

| Параметр     | Значение по умолчанию                                                                            | Описание                                                                                                                                                                                                                                                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fetch`      | глобальный fetch                                                                                 | Функция с интерфейсом [Fetch API](https://developer.mozilla.org/ru/docs/Web/API/Fetch_API). Если глобальный fetch не найден, то будет выброшена ошибка при попытке осуществить http запрос. Начиная с Node.js 18 [fetch](https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#fetch) является частью стандратной библиотеки. |
| `endpoint`   | `"https://api.moysklad.ru/api"`                                                                  | Точка досупа к API (хост точки доступа можно указать через переменную окружения `MOYSKLAD_HOST`, по умолчанию `api.moysklad.ru`)                                                                                                                                                                                                   |
| `api`        | `"remap"`                                                                                        | Раздел API (можно задать через переменную окружения `MOYSKLAD_API`)                                                                                                                                                                                                                                                                |
| `apiVersion` | `"1.2"`                                                                                          | Версия API (можно задать через переменную окружения `MOYSKLAD_{NAME}_API_VERSION`, где `{NAME}` - название API в верхнем регистре, напр. `MOYSKLAD_REMAP_API_VERSION`)                                                                                                                                                             |
| `token`      | `undefined`                                                                                      | Токен доступа к API (см. [Аутентификация](#аутентификация))                                                                                                                                                                                                                                                                        |
| `login`      | `undefined`                                                                                      | Логин для доступа к API (см. [Аутентификация](#аутентификация))                                                                                                                                                                                                                                                                    |
| `password`   | `undefined`                                                                                      | Пароль для доступа к API (см. [Аутентификация](#аутентификация))                                                                                                                                                                                                                                                                   |
| `emitter`    | `undefined`                                                                                      | экземляр [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) для передачи [событий библиотеки](#события)                                                                                                                                                                                                  |
| `userAgent`  | `moysklad/{ver} (+https://github.com/wmakeev/moysklad)`, где `{ver}` - текущая версия библиотеки | Содержимое заголовка "User-Agent" при выполнении запроса. Удобно использовать для контроля изменений через API на вкладке "Аудит". Можно задать через переменную окружения `MOYSKLAD_USER_AGENT`.                                                                                                                                  |

Явное задание параметра переопределяет значение заданное в соотв. переменной окружения.

**Пример использования:**

```js
const Moysklad = require('moysklad')

// Явное указание используемой версии API
const moysklad = Moysklad({ apiVersion: '1.2' })
```

## Аутентификация

Есть несколько способов передачи параметров аутентификации:

1. Напрямую при инициализации экземпляра

   ```js
   // Аутентификация по логину и паролю
   const moysklad = Moysklad({ login, password })
   ```

   ```js
   // Аутентификация по токену
   const moysklad = Moysklad({ token })
   ```

2. Через глобальные переменные или переменные окружения

   Если параметры аутентификации не указаны при инициализации клиента,

   ```js
   const moysklad = Moysklad()
   ```

   то будет проведен поиск параметров в следующем порядке:

   1. Переменная окружения `process.env.MOYSKLAD_TOKEN`
   2. Переменные окружения `process.env.MOYSKLAD_LOGIN` и `process.env.MOYSKLAD_PASSWORD`
   3. Глобальная переменная `window.MOYSKLAD_TOKEN`
   4. Глобальные переменные `window.MOYSKLAD_LOGIN` и `window.MOYSKLAD_PASSWORD`
   5. Глобальная переменная `global.MOYSKLAD_TOKEN`
   6. Глобальные переменные `global.MOYSKLAD_LOGIN` и `global.MOYSKLAD_PASSWORD`

## Статические методы

### getTimeString

> Преобразует локальную дату в строку в формате API МойСклад в часовом поясе Москвы

```ts
Moysklad.getTimeString(date: Date, includeMs?: boolean): string
```

**Параметры:**

`date` - дата

`includeMs` - если `true`, то в дату будут включены миллисекунды

**Пример использования:**

```js
const date = new Date('2017-02-01T07:10:11.123Z')
const timeString = Moysklad.getTimeString(date, true)

assert.equal(timeString, '2017-02-01 10:10:11.123')
```

### parseTimeString

> Преобразует строку с датой в формате API МойСклад в объект даты (с учетом локального часового пояса и часового пояса API МойСклад)

```ts
Moysklad.parseTimeString(date: string) : Date
```

**Параметры:**

`date` - дата в формате МойСклад (напр. `2017-04-08 13:33:00.123`)

**Пример использования:**

```js
const parsedDate = Moysklad.parseTimeString('2017-04-08 13:33:00.123')

assert.equal(parsedDate.toISOString(), '2017-04-08T10:33:00.123Z')
```

### parseUrl (статический метод)

> Разбор url на составные компоненты

Аналогичен [parseUrl](#parseurl) методу экземпляра, за тем исключением, что
на вход принимает только строку в формате href МойСклад.

### buildFilter

> Возвращает строку фильтра по объекту `QueryFilter` (см. [filter](#filter))

```js
Moysklad.buildFilter({ name: { $st: 'foo' } })
// 'code=123;name~=foo'
```

### buildQuery

> Формирует строку с параметрами запроса по объекту `Query` (см. [query](#query))

```js
Moysklad.buildQuery({
  filter: { name: 'foo' },
  limit: 100,
  foo: 'bar'
})

// 'filter=name%3Dfoo&limit=100&foo=bar'
```

## Методы экземпляра

### GET

> GET запрос

```ts
ms.GET(path: string, query?: object, options?: object): Promise
```

**Параметры:**

`path` - [url ресурса](#path)

`query` - [параметры запроса](#query)

`options` - [опции запроса](#options-параметры-запроса)

**Пример использования:**

```js
const productsCollection = await ms.GET('entity/product', { limit: 50 })

const order = await ms.GET(`entity/customerorder/${orderId}`, {
  expand: 'positions'
})
```

### POST

> POST запрос

```ts
ms.POST(
  path: string,
  payload?: object | Array<object>,
  query?: object,
  options?: object
): Promise
```

**Параметры:**

`path` - [url ресурса](#path)

`payload` - объект или коллекция объектов (будет преобразовано в строку методом `JSON.stringify`)

`query` - [параметры запроса](#query)

`options` - [опции запроса](#options-параметры-запроса)

**Пример использования:**

```js
const newProduct = await ms.POST('entity/product', { name: 'Новый товар' })
```

По умолчанию, при массовом обновлении сущностей, если _хотябы один_ из элементов в ответе содержит ошибку, то метод выбросит ошибку
[MoyskladCollectionError](#moyskladcollectionerror) .

Если такое поведение не является предпочтительным, то можно обрабатывать ошибки при массовом обновлении/создании объектов вручную (см. `muteCollectionErrors` в [параметрах запроса](#options-параметры-запроса)):

```js
const updated = await ms.POST('entity/supply', supplyList, null, {
  muteCollectionErrors: true
})

const errors = updated
  .filter(item => item.errors)
  .map(item => item.errors[0].error)

if (errors.length) {
  console.log('Есть ошибки:', errors.join(', '))
}

const supplyHrefs = updated
  .filter(item => !item.errors)
  .map(item => item.meta.href)
```

### PUT

> PUT запрос

```ts
ms.PUT(
  path: string | string[],
  payload?: object,
  query?: object,
  options?: object
) : Promise
```

**Параметры:**

`path` - [url ресурса](#path)

`payload` - обнвляемый объект (будет преобразован в строку методом `JSON.stringify`)

`query` - [параметры запроса](#query)

`options` - [опции запроса](#options-параметры-запроса)

**Пример использования:**

```js
const updatedProduct = await ms.PUT(`entity/product/${id}`, product)
```

### DELETE

> DELETE запрос

```ts
ms.DELETE(path: string, options?: object): Promise
```

**Параметры:**

`path` - [url ресурса](#path)

`options` - [опции запроса](#options-параметры-запроса)

Метод `DELETE` возвращает `undefined` при успешном запросе.

**Пример использования:**

```js
await ms.DELETE(`entity/product/${product.id}`)
```

### getOptions

> Возвращает опции переданные в момент инициализации экземпляра библиотеки

**Пример использования:**

```js
const options = {
  login: 'login',
  password: 'password'
}

const ms = Moysklad(options)

const msOptions = ms.getOptions()

assert.ok(msOptions !== options)
assert.equal(msOptions.login, 'login')
assert.equal(msOptions.password, 'password')
```

### buildUrl

> Формирует url запроса

```ts
ms.buildUrl(url: string, query?: object): string
```

**Параметры:**

`url` - полный url (должен соответствовать настройкам)

`path` - [url ресурса](#path)

`query` - [параметры запроса](#query)

**Пример использования:**

```js
const url = ms.buildUrl(
  'https://api.moysklad.ru/api/remap/1.2/entity/customerorder?expand=positions',
  { limit: 100 }
)

assert.equal(
  url,
  'https://api.moysklad.ru/api/remap/1.2/entity/customerorder?expand=positions&limit=100'
)
```

```js
const url = ms.buildUrl('entity/customerorder', { expand: 'positions' })

assert.equal(
  url,
  'https://api.moysklad.ru/api/remap/1.2/entity/customerorder?expand=positions'
)
```

Можно безопасно дублировать символы `/`, лишние знаки будут исключены из
результирующего url

```js
const positionUrl = `/positions/${posId}/`

const url = ms.buildUrl(`entity/customerorder/` + positionUrl)

assert.equal(url, `https://api.moysklad.ru/api/remap/1.2/entity/customerorder/positions/${posId}`)
```

### parseUrl

> Разбор url на составные компоненты

```ts
ms.parseUrl(url: string): {
  endpoint: string
  api: string
  apiVersion: string
  path: Array<string>
  query: object
}
```

**Параметры:**

`url` - url ресурса

**Пример использования:**

```js
const parsedUri = ms.parseUrl('https://api.moysklad.ru/api/remap/1.2/entity/customerorder?expand=positions')

assert.deepEqual(parsedUri, {
  endpoint: 'https://api.moysklad.ru/api',
  api: 'remap'
  apiVersion: '1.2',
  path: ['entity', 'customerorder'],
  query: {
    expand: 'positions'
  }
})
```

### fetchUrl

> Выполнить запрос по указанному url

```ts
ms.fetchUrl(url: string, options?: object): Promise
```

**Параметры:**

`url` - url ресурса

`options` - [опции запроса](#options-параметры-запроса)

**Пример использования:**

```js
const url = `https://api.moysklad.ru/api/remap/1.2/entity/customerorder/eb7bcc22-ae8d-11e3-9e32-002590a28eca`

const patch = { applicable: false }

const updatedOrder = await ms.fetchUrl(url, {
  method: 'PUT',
  body: JSON.stringify(patch)
})
```

### Основные аргументы

#### `path`

Строка.

**Примеры:**

Url запроса можно указать полностью

```js
ms.GET(
  `https://api.moysklad.ru/api/remap/1.2/entity/customerorder/${ORDER_ID}/positions/${POSITION_ID}?expand=assortment`
)
```

Но гораздо удобнее указывать путь только после версии API и выносить
параметры запроса в параметры метода. Полный url будет сгенерирован автоматически, согласно [настройкам экземпляра](#параметры-инициализации).

Ниже пример аналогичного запроса:

```js
ms.GET(`entity/customerorder/${ORDER_ID}/positions/${POSITION_ID}`, {
  expand: 'assortment'
})
```

Можно безопасно дублировать символы `/`, лишние знаки будут сключены из
результирующего url

```js
const positionUrl = `/positions/${posId}`

ms.GET(`entity/customerorder/` + positionUrl)
```

#### `query`

##### querystring

Все поля объекта запроса преобразуются в соответствующую строку запроса url. Некоторые поля могут подвергаться преобразованию (напр. поля [`filter`](#filter) и [`order`](#order)).

Поле объекта запроса должно иметь тип: `string`, `number`, `boolean`, `null` или `undefined`, любое другое значение вызовет ошибку.

```js
const query = {
  str: 'some string',
  num: 1,
  bool: true,
  nil: null, // будет добавлено в строку запроса с пустым значением
  nothing: undefined, // поле будет пропущено
  arr: ['str', 1, true, null, undefined]
}

// https://api.moysklad.ru/api/remap/1.2/entity/demand?str=some%20string&num=1&bool=true&nil=&arr=str&arr=1&arr=true&arr=
ms.GET('entity/demand', query)
```

##### filter

Если поле `filter` объект, то вложенные поля `filter` преобразуются в параметры фильтра в строке запроса в соответствии со следующими правилами:

- `string`, `number`, `boolean` не проходят дополнительных преобразований (`key=value`)
- `null` преобразуется в пустую строку (`key=`)
- `Date` преобразуется в строку методом [`getTimeString`](#gettimestring) (`key=YYYY-MM-DD HH:mm:ss`)
- `object` интерпретируется как набор селекторов или вложенных полей (см. пример ниже)

Пример фильтра:

```js
const query = {
  filter: {
    name: '00001',
    code: [1, 2, '03'],
    foo: new Date(2000, 0, 1),
    state: {
      name: 'Оформлен'
    },
    moment: {
      $gt: new Date(2000, 0, 1),
      $lte: new Date(2001, 0, 2, 10, 0, 15, 123)
    },
    bar: {
      baz: 1,
      $exists: true
    }
  }
}
```

соответствует следующему значению поля `filter` в запросе (даты переданы в часовом поясе +5):

```
bar!=;bar.baz=1;code=03;code=1;code=2;foo=1999-12-31 22:00:00;moment<=2001-01-02 08:00:15.123;moment>1999-12-31 22:00:00;name=00001;state.name=Оформлен
```

Для построения фильтра можно использовать селекторы в стиле Mongo (как в примере выше).

Подробное описание всех возможных селекторов:

| Селектор                             | Фильтр МойСклад               | Описание                   |
| ------------------------------------ | ----------------------------- | -------------------------- |
| `key: { $eq: value }`                | `key=value`                   | равно                      |
| `key: { $ne: value }`                | `key!=value`                  | не равно                   |
| `key: { $gt: value }`                | `key>value`                   | больше                     |
| `key: { $gte: value }`               | `key>=value`                  | больше или равно           |
| `key: { $lt: value }`                | `key<value`                   | меньше                     |
| `key: { $lte: value }`               | `key<=value`                  | меньше или равно           |
| `key: { $st: value }`                | `key~=value`                  | начинается со строки       |
| `key: { $et: value }`                | `key=~value`                  | заканчивается строкой      |
| `key: { $contains: value }`          | `key~value`                   | содержит строку            |
| `key: { $in: [..] }` или `key: [..]` | `key=value1;key=value2;...`   | входит в                   |
| `key: { $nin: [..] }`                | `key!=value1;key!=value2;...` | не входит в                |
| `key: { $exists: true }`             | `key!=`                       | наличие значения (не null) |
| `key: { $exists: false }`            | `key=`                        | пустое значение (null)     |
| `key: { $all: [{..}, ..] }`          |                               | объединение условий        |
| `key: { $not: {..} }`                |                               | отрицание условия          |

На один ключ можно использовать несколько селекторов.

Подробнее с правилами фильтрации можно ознакомится в документации МойСклад:

- [Фильтрация выборки с помощью параметра filter](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter)
- [Оператор фильтрации "подобие"](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-operator-fil-tracii-quot-podobie-quot)
- [Фильтрация](https://dev.moysklad.ru/doc/api/remap/1.2/workbook/#workbook-fil-traciq-listanie-poisk-i-sortirowka-fil-traciq)

##### order

Если поле `order` массив, то произойдет преобразование записи из формы массива в строку.

Примеры:

- `['name']` → `'name'`
- `[['code','desc']]` → `'code,desc'`
- `['name', ['code','desc']]` → `'name;code,desc'`
- `['name,desc', ['code','asc'], ['moment']]` → `'name,desc;code,asc;moment'`

👉 [examples/query.js](https://github.com/wmakeev/moysklad/blob/master/examples/query.js)

##### expand и limit

Если указано значение expand, но не указан limit, то в поле limit по умолчанию будет подставлено значение `100`. Это важно, т.к. в версии API remap 1.2 expand не работает, если не указан limit.

#### `options` (параметры запроса)

Все опции переданные в объекте `options` (за исключением описанных ниже) передаются напрямую в опции метода `fetch` ([Fetch API](http://github.github.io/fetch/)) при осуществлении запроса.

С опциями fetch API можно ознакомиться по [этой ссылке](https://github.com/node-fetch/node-fetch#options)

Опции специфичные для библиотеки moysklad (не передаются в `fetch`):

| Поле                        | Тип       | Описание                                                                                                                                                                                                                                                                                                                      |
| --------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rawResponse`               | `boolean` | Если `true`, то метод вернет результат в виде объекта [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). Код и содержимое ответа не проверяется на ошибки.                                                                                                                                                |
| `rawRedirect`               | `boolean` | Если `true` и код запроса `3xx` (редирект), то метод вернет [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). В обратном случае, будет выброшена ошибка `MoyskladRequestError`. Опция нужна для явного указания того, что вы ожидаете получить редирект при опции запроса `redirect` не равной `follow`. |
| `muteApiErrors`             | `boolean` | Если `true`, то все ошибки API будут проигнорированы (метод не будет генерировать ошибку если код ответа сервера не в диапазоне 200-299 и тело ответа содержит описание ошибки МойСклад). Опция не затрагивает прочие ошибки, которые не возвращают JSON ответ.                                                               |
| `muteCollectionErrors`      | `boolean` | Если `true`, то все ошибки внутри коллекций при массовом обновлении сущностей будут проигнорированы.                                                                                                                                                                                                                          |
| `precision`                 | `boolean` | Если `true`, то в запрос будет включен заголовок `X-Lognex-Precision` со значением `true` (отключение округления цен и себестоимости до копеек).                                                                                                                                                                              |
| `webHookDisable`            | `boolean` | Если `true`, то в запрос будет включен заголовок `X-Lognex-WebHook-Disable` со значением `true` (отключить уведомления вебхуков в контексте данного запроса).                                                                                                                                                                 |
| `downloadExpirationSeconds` | `number`  | Устанавливает значение для заголовока `X-Lognex-Download-Expiration-Seconds` (подробнее см. [Ссылки на файлы](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-ssylki-na-fajly))                                                                                                                 |

<details>
  <summary>Примеры</summary>

- Формирование заполненного шаблона печатной формы и получение ссылки для загрузки ([examples/download-print-form.js](https://github.com/wmakeev/moysklad/blob/master/examples/download-print-form.js)):

  ```js
  const path = require('node:path')
  const { writeFile } = require('node:fs/promises')
  const { fetch } = require('undici')

  /** @type {import('..')} */
  const Moysklad = require('moysklad')

  const TEMPLATE_ID = '8a686b8a-9e4a-11e5-7a69-97110004af3e'
  const DEMAND_ID = '13abf361-e9c6-45ea-a940-df70289a7f95'

  async function downloadPrintForm() {
    const ms = Moysklad({ fetch })

    const body = {
      template: {
        meta: {
          href: ms.buildUrl(
            `entity/demand/metadata/customtemplate/${TEMPLATE_ID}`
          ),
          type: 'customtemplate',
          mediaType: 'application/json'
        }
      },
      extension: 'pdf'
    }

    /** @type {import('undici').Response} */
    const response = await ms.POST(
      `entity/demand/${DEMAND_ID}/export`,
      body,
      null,
      // вернуть результат запроса с редиректом без предварительного разбора
      { rawRedirect: true }
    )

    const location = response.headers.get('location')

    console.log(location)
    // 'https://print-prod.moysklad.ru/temp/.../00123.pdf'

    const formResponse = await fetch(location)

    // TODO undici возвращает веб-стримы (апгрейдится до 18-й ноды пока рановато)

    const blob = await formResponse.blob()

    const buffer = Buffer.from(await blob.arrayBuffer())

    await writeFile(path.join(process.cwd(), '__temp/form.pdf'), buffer)
  }

  downloadPrintForm()
  ```

- Указание кастомного заголовка

  ```js
  const ms = Moysklad({ fetch: require('node-fetch') })

  const folder = {
    meta: {
      type: 'productfolder',
      href: ms.buildUrl(`entity/productfolder/${FOLDER_ID}`)
    },
    description: 'Новое описание группы товаров'
  }

  // Указываем кастомный заголовок X-Lognex-WebHook-Disable для PUT запроса
  const updatedFolder = await ms.PUT(
    `entity/productfolder/${FOLDER_ID}`,
    folder,
    null,
    {
      // вместо этого можно использовать webHookDisable: true
      headers: {
        'X-Lognex-WebHook-Disable': true
      }
    }
  )

  assert.equal(updatedFolder.description, folder.description)
  ```

- Автоматический редирект

  Идентификаторы товаров в приложении МойСклад отличаются от идентификаторов в API. Поэтому, при запросе товара по id из приложения, будет выполнен редирект на другой href.

  ```js
  const ms = Moysklad({ fetch })

  // https://api.moysklad.ru/app/#good/edit?id=cb277549-34f4-4029-b9de-7b37e8e25a54
  const PRODUCT_UI_ID = 'cb277549-34f4-4029-b9de-7b37e8e25a54'

  // Error: 308 Permanent Redirect
  await ms.fetchUrl(ms.buildUrl(`entity/product/${PRODUCT_UI_ID}`))

  // Указана опция redirect
  const product = await ms.fetchUrl(
    ms.buildUrl(`entity/product/${PRODUCT_UI_ID}`),
    { redirect: 'follow' }
  )

  assert.ok(product) // OK
  ```

</details>

## События

| Событие         | Передаваемый объект                           | Момент наступления            |
| --------------- | --------------------------------------------- | ----------------------------- |
| `request`       | `{ requestId, url, options }`                 | Отправлен http запрос         |
| `response`      | `{ requestId, url, options, response }`       | Получен ответ на запрос       |
| `response:body` | `{ requestId, url, options, response, body }` | Загружено тело ответа         |
| `error`         | `Error`, `{ requestId }`                      | Ошибка при выполнении запроса |

Пример использования:

```js
const { fetch } = require('undici')
const { EventEmitter } = require('events')
const Moysklad = require('moysklad')

/** @type {Moysklad.MoyskladEmitter} */
const emitter = new EventEmitter()

const ms = Moysklad({ fetch, emitter })

emitter
  .on('request', ({ requestId, url, options }) => {
    console.log(`${requestId} ${options.method} ${url}`)
  })
  .on('error', (err, { requestId }) => {
    console.log(requestId, err)
  })

ms.GET('entity/customerorder', { limit: 1 }).then(res => {
  console.log('Order name: ' + res.rows[0].name)
})
```

Более подробный пример смотрите в [examples/events.js](https://github.com/wmakeev/moysklad/blob/master/examples/events.js).

## Работа с ошибками

В рамках работы с библиотекой выделены следующие виды ошибок:

| №   | Название ошибки         | Класс ошибки                                                        | Описание                                                                                                                                           |
| --- | ----------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | **Ошибка библиотеки**   | [MoyskladError](#moyskladerror)                                     | Напр. не верно указаны параметры одного из методов.                                                                                                |
| 2   | **Ошибка запроса**      | [MoyskladRequestError](#moyskladrequesterror)                       | Ответ получен с кодом ошибки, тело ответа НЕ содержит JSON с описанием ошибки в формате МойСклад.                                                  |
| 3   | **Ошибка API МойСклад** | [MoyskladApiError](#moyskladapierror)                               | Ответ получен с кодом ошибки, тело ответа содержит JSON с описанием ошибки в формате МойСклад.                                                     |
| 4   | **Ошибка в коллекции**  | [MoyskladCollectionError](#moyskladcollectionerror)                 | Ошибка в одном из элементов внутри коллекции.                                                                                                      |
| 5   | **Неявный редирект**    | [MoyskladUnexpectedRedirectError](#moyskladunexpectedredirecterror) | Ошибка возникает когда запрос вернул перенапраление (код `3xx`) и явно не указана опция запроса `rawRedirect` (опция `redirect` не равна `follow`) |

Библиотека дает возможность указать параметры запроса `muteApiErrors` и `muteCollectionErrors` для игнорирования ошибок API п.3 и п.4 соответственно.

Ошибки глобального fetch модуля или переданного при инициализации экземпляра не перехватываются внутри библиотеки. Т.е. все описанные выше ошибки, связанные с выполнением запроса, формируются уже после анализа полученного ответа.

### MoyskladError

> Внутренняя ошибка библиотеки не связанная с выполнением запроса к API

Наследует класс `Error`

<details>
  <summary>Примеры</summary>

Код с ошибкой:

```js
await ms.GET('entity/product', {
  filter: 123
})
```

Структура ошибки:

```json
{
  "name": "MoyskladError",
  "message": "Поле filter запроса должно быть строкой или объектом"
}
```

</details>

### MoyskladRequestError

> Ошибка при выполнении запроса

Наследует класс [MoyskladError](#moyskladerror)

<details>
  <summary>Примеры</summary>

Код с ошибкой:

```js
const ms = Moysklad({ fetch, api: 'foo', apiVersion: '0' })

await ms.GET('foo/bar')
```

Структура ошибки:

```json
{
  "name": "MoyskladRequestError",
  "message": "404 Not Found",
  "url": "https://api.moysklad.ru/api/foo/0/foo/bar",
  "status": 404,
  "statusText": "Not Found"
}
```

</details>

### MoyskladApiError

> Ошибка API МойСклад

Наследует класс [MoyskladRequestError](#moyskladrequesterror)

Ошибка формируется в случае, если API помимо HTTP кода ошибки, так же вернуло стандартное описание ошибки МойСклад в формате JSON. В обратном случае (ответ не содержит JSON с ошибкой) будет выброшена ошибка [MoyskladRequestError](#moyskladrequesterror)

<details>
  <summary>Примеры</summary>

Код с ошибкой:

```js
await ms.GET('entity/product2')
```

Структура ошибки:

```json
{
  "name": "MoyskladApiError",
  "message": "Неизвестный тип: 'product2' (https://dev.moysklad.ru/doc/api/remap/1.2/#error_1005)",
  "url": "https://api.moysklad.ru/api/remap/1.2/entity/product2",
  "status": 412,
  "statusText": "Precondition Failed",
  "code": 1005,
  "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_1005",
  "errors": [
    {
      "error": "Неизвестный тип: 'product2'",
      "code": 1005,
      "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_1005"
    }
  ]
}
```

Можно игнорировать ошибку API, указав `muteApiErrors:true` в опциях запроса.

```js
const rawError1 = await ms.GET('entity/product2', null, {
  muteApiErrors: true
})

console.log(rawError1.errors[0].error)
// Неизвестный тип: 'product2'
```

</details>

### MoyskladCollectionError

> Ошибка в коллекции при массовом создании/изменении сущностей

Наследует класс [MoyskladApiError](#moyskladapierror)

Ошибка выбрасывается когда возвращаемая коллекция содержит хотя бы одну ошибку.

Например, когда при массовом обновлении нескольких объектов часть из них не были обновлены, то API вернет массив с результатами в части которых будет указана ошибка.

<details>
  <summary>Примеры</summary>
Код с ошибкой:

```js
await ms.POST('entity/product', [
  { foo: 'bar' },
  {
    meta: {
      type: 'product',
      href: ms.buildUrl(`entity/product/${uuidFromApi}`)
    },
    weight: 42
  },
  { name: 123 }
])
```

Структура ошибки:

```json
{
  "name": "MoyskladCollectionError",
  "message": "Ошибка сохранения объекта: поле 'name' не может быть пустым или отсутствовать (https://dev.moysklad.ru/doc/api/remap/1.2/#error_3000)",
  "url": "https://api.moysklad.ru/api/remap/1.2/entity/product",
  "status": 400,
  "statusText": "Bad Request",
  "code": 3000,
  "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_3000",
  "line": 1,
  "column": 3,
  "errors": [
    {
      "error": "Ошибка сохранения объекта: поле 'name' не может быть пустым или отсутствовать",
      "code": 3000,
      "parameter": "name",
      "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_3000",
      "line": 1,
      "column": 3
    },
    {
      "error": "Ошибка формата: значение поля 'name' не соответствует типу строка",
      "code": 2016,
      "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_2016",
      "line": 1,
      "column": 169
    }
  ],
  "errorsIndexes": [
    [
      0,
      [
        {
          "error": "Ошибка сохранения объекта: поле 'name' не может быть пустым или отсутствовать",
          "code": 3000,
          "parameter": "name",
          "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_3000",
          "line": 1,
          "column": 3
        }
      ]
    ],
    [
      2,
      [
        {
          "error": "Ошибка формата: значение поля 'name' не соответствует типу строка",
          "code": 2016,
          "moreInfo": "https://dev.moysklad.ru/doc/api/remap/1.2/#error_2016",
          "line": 1,
          "column": 169
        }
      ]
    ]
  ]
}
```

Можно игнорировать ошибки в коллекции, указав `muteCollectionErrors:true`
в опциях запроса.

```js
const result2 = await ms.POST(
  'entity/product',
  [
    { foo: 'bar' },
    {
      meta: {
        type: 'product',
        href: ms.buildUrl(`entity/product/${uuidFromApi}`)
      },
      weight: 42
    },
    { name: 123 }
  ],
  null,
  {
    muteCollectionErrors: true
  }
)

const collItemError = result2.find(it => it.errors)

if (collItemError) {
  console.log(collItemError.errors[0].error)
  // Ошибка сохранения объекта: поле 'name' не может быть пустым или отсутствовать
}
```

</details>

### MoyskladUnexpectedRedirectError

> Ошибка если запрос вернул перенапраление (код `3xx`), когда явно не указана опция запроса `rawRedirect` и опция `redirect` не равна `follow`

Наследует класс [MoyskladRequestError](#moyskladrequesterror)

<details>
  <summary>Примеры</summary>

```js
/** id товара из приложения МойСклад */
const uuidFromApp = 'cb277549-34f4-4029-b9de-7b37e8e25a54'

/** id товара из API (отличается от id из приложения) */
let uuidFromApi

const getProduct = id => ms.GET(`entity/product/${id}`)

try {
  await getProduct(uuidFromApp)
} catch (err) {
  if (err instanceof Moysklad.MoyskladUnexpectedRedirectError) {
    uuidFromApi = ms.parseUrl(err.location).path.pop()
    await getProduct(uuidFromApi)
  } else {
    throw err
  }
}
```

Можно обработать перенаправление без перехвата ошибки:

```js
const { fetch, Response } = require('undici')

const ms = Moysklad({ fetch })

let product = await ms.GET(`entity/product/${uuidFromApp}`, null, {
  rawRedirect: true
})

if (product instanceof Response) {
  uuidFromApi = ms.parseUrl(product.headers.get('location')).path.pop()

  product = await ms.GET(`entity/product/${uuidFromApi}`)
}

console.log(product.id === uuidFromApp) // false
```

Или использовать автоматическое пренаправление, указав значение `follow` в опции `redirect`:

```js
const product = await ms.GET(`entity/product/${uuidFromApp}`, null, {
  redirect: 'follow'
})

console.log(product.id === uuidFromApp) // false
```

</details>

## История изменений

[CHANGELOG.md](https://github.com/wmakeev/moysklad/blob/master/CHANGELOG.md)

## Планы развития

Планируется немного переработанная версия библиотеки в другом репозитории и npm пакете. Без концептуальных изменений, но с убранным легаси кодом.

- Переписать на TypeScript
- Добавить новый метод для формирования объекта запроса
- Убрать всё легаси (в том числе то, что тянет лишние зависимости - "have2" и "stampit")
- Более развернутая документация с автогенерацией части описаний методов

## TODO

Свалка мыслей по развитию библиотеки - [TODO.md](https://github.com/wmakeev/moysklad/blob/master/TODO.md)