3 Commits

Author SHA1 Message Date
apilki
e1e04dd2bc release 0.3.0 2026-08-29 15:07:33 +00:00
apilki
3db10ca0b1 release 0.2.0 2026-08-26 15:10:38 +00:00
apilki
dc9862c368 release 0.2.0 2026-08-21 21:08:25 +00:00
6 changed files with 324 additions and 38 deletions

136
README.md
View File

@@ -1,15 +1,37 @@
# @apilki/yandex-market — JavaScript/TypeScript клиент
# @apilki/yandex-market — JavaScript/TypeScript client
**Версия: 0.1.0 · API от 2026-08-20** <!-- штампуется пайплайном (github-distro) -->
**Version: 0.3.0 · API as of 2026-08-25** <!-- штампуется пайплайном (github-distro); штамп ОДИН (EN), стандарт public-client-readme-standard -->
JavaScript/TypeScript клиент для **Yandex Market Partner API** (продавец-сторона: заказы, каталог, цены, отчёты). Построен на базе `fetch` API. Работает и в Node.js, и в браузере. Публикуется в npm-скоупе `@apilki` (GitHub-орг `apilki`).
A **curated generated TypeScript client** for the Yandex Market Partner API —
generated from the official API specification, with edge-cases and conflicts
resolved. Fully typed, fetch-based, zero runtime dependencies, works in
Node.js ≥ 18 and browsers. Covers all 161 operations (43 API classes) and is
regenerated on every upstream change.
## Особенности сборки
- **Типизация**: Полная поддержка TypeScript (`.d.ts` включены).
- **Стиль имен**:
- Методы и классы: `camelCase` (например, `orderApi.getOrders`).
- Свойства объектов (JSON): **Original** (сохранено именование из спецификации Яндекса, обычно это `camelCase`).
- **Аргументы методов**: Плоский список (позиционный). Первым параметром в большинстве методов идет `campaignId`.
```bash
npm install @apilki/yandex-market
```
Docs: https://yandex.ru/dev/market/partner-api/doc/ru/ · Full description (RU) below.
## Что это
Готовый TypeScript-клиент для **Yandex Market Partner API** — заказы, каталог,
цены, отчёты (продавец-сторона). Сгенерирован из официальной документации
Яндекса и **доведён до ума**: неоднозначности и конфликты документации
разбираются агентами и автоматическими рецептами, результат проверяется
тестами — вы получаете клиент, который просто работает.
- **Полное покрытие** — все 161 операция, 43 API-класса, ~757 моделей;
- **Всегда свежий** — перегенерируется при каждом изменении API Яндекса;
- **Никаких зависимостей** — на чистом `fetch`, работает в Node.js ≥ 18 и в браузере;
- **Типизация / Ноль сюрпризов** — полные `.d.ts`, оригинальные имена полей из спецификации.
## Установка
```bash
npm install @apilki/yandex-market
```
## Быстрый старт
@@ -32,32 +54,43 @@ const orderApi = new OrderApi(config);
const campaignsApi = new CampaignsApi(config);
```
## 2. Работа с несколькими кабинетами или магазинами
### 2. Несколько кабинетов или магазинов
В зависимости от метода API Яндекса, первым аргументом может выступать либо **campaignId** (идентификатор магазина), либо **businessId** (идентификатор бизнеса/кабинета). Ключ авторизации `Api-Key` уже находится в заголовках конфигурации, поэтому он не требуется в аргументах.
В зависимости от метода API Яндекса первым аргументом выступает либо
**campaignId** (идентификатор магазина), либо **businessId** (идентификатор
бизнеса/кабинета). Ключ авторизации `Api-Key` уже в заголовках конфигурации —
в аргументах он не требуется.
```javascript
const campaignId = 12345678;
const campaignId = 12345678;
const businessId = 987654;
async function fetchData() {
try {
// Пример метода с campaignId (Заказы)
const orders = await orderApi.getOrders(campaignId, { status: 'PROCESSING' });
// Метод с campaignId (Заказы)
const orders = await orderApi.getOrders(campaignId, { status: 'PROCESSING' });
// Пример метода с businessId (Каталог/Цены)
const offers = await assortmentApi.getOfferMappings(businessId, { limit: 100 });
console.log('Данные получены');
} catch (error) {
console.error('Yandex API Error:', error);
}
// Метод с businessId (Каталог/Цены)
const offers = await assortmentApi.getOfferMappings(businessId, { limit: 100 });
console.log('Данные получены');
}
```
### 3. Обработка ошибок
## Особенности сборки
В случае ошибок 4xx/5xx клиент выбрасывает ResponseError. Ответ Яндекса обычно содержит объект с массивом ошибок.
- **Типизация**: полная поддержка TypeScript (`.d.ts` включены).
- **Стиль имён**:
- Методы и классы: `camelCase` (например, `orderApi.getOrders`);
- Свойства объектов (JSON): **original** — сохранено именование из спецификации Яндекса.
- **Аргументы методов**: плоский список (позиционный). Первым параметром в большинстве
методов идёт `campaignId`, в методах каталога/финтеха — `businessId`. Проверяйте
сигнатуру через автодополнение в IDE.
- **Fetch**: в среде Node.js < 18 требуется полифил `node-fetch`.
## Ошибки
При ответах 4xx/5xx клиент выбрасывает `ResponseError`. Ответ Яндекса обычно
содержит объект с массивом ошибок.
```javascript
try {
@@ -69,7 +102,7 @@ try {
}
```
### 4. Отладка (Middleware)
## Отладка (Middleware)
Для отладки сетевых запросов используйте Middleware:
@@ -84,21 +117,52 @@ const config = new Configuration({
});
```
### Важные замечания
## Покрытие API
Порядок аргументов: В большинстве методов Яндекса первым аргументом идет campaignId. Всегда проверяйте сигнатуру метода через автодополнение в IDE.
Клиент покрывает **161 операцию** в **43 API-классах** (~757 моделей данных) —
полный охват Yandex Market Partner API. Покрытие поддерживается пайплайном:
каждое изменение спецификации проверяется автоматическими тестами на полноту.
**Идентификаторы (ID):** Внимательно следите за сигнатурой метода в IDE.
* Если метод относится к операциям магазина — первым аргументом идет campaignId.
* Если к настройкам каталога или финтеху — первым аргументом идет businessId.
## Postman-коллекция
**JSON:** Мы сохранили оригинальное именование полей. Если поле в документации Яндекса называется deliveryServiceId, в коде оно будет точно таким же.
Готовая [Postman Collection](https://github.com/apilki/yandex-market-postman)
v2.1 — отдельный артефакт `apilki/yandex-market-postman`
(`postman/collection.json`). Импортируйте коллекцию и работайте с API без кода.
**Fetch:** В среде Node.js < 18 требуется полифил node-fetch.
## Тесты
**Авторизация:** Если Api-Key не задан в Configuration, он может потребоваться первым аргументом в каждом методе. Проверяйте подсказки IDE.
После установки выполните `npm test` — юнит-набор по ключевым путям
(запросы, сериализация, обработка ошибок, middleware). Прогоняется перед
каждой публикацией, работает офлайн — без зависимостей и ключей доступа.
### Ссылки
## Известные ограничения
* [Официальная документация API Яндекс Маркета для продавцов](https://yandex.ru/dev/market/partner-api/doc/ru/)
* [Спецификация API Яндекс Маркета для продавцов](https://github.com/yandex-market/yandex-market-partner-api)
- **Свежесть API — дата коммита спецификации, а не номер версии.** Апстрим
публикует спецификацию без семантических версий (`LATEST`): номер версии
есть только у клиента. Поэтому актуальность данных отражается датой в шапке
README (строка «API от …») и в CHANGELOG релизов — сопоставление по версиям
спецификации невозможно. Breaking-изменения анонсируются в ⚠-секции
CHANGELOG и в [MIGRATION.md](MIGRATION.md) (в пакете и GH-дистро).
- **Node.js < 18 — нужен полифил `fetch`.** Клиент работает на встроенном
`fetch` (ноль зависимостей), нативно доступном с Node 18. На более старых
версиях установите `node-fetch` и передайте его через
`Configuration.fetchApi` (подробнее — «Особенности сборки»).
- **Типы коллекционных параметров различаются (`Array` / `Set`)** — наследие
генерации: часть методов принимает массивы, часть `Set`. Ориентируйтесь на
сигнатуру метода (подсказки IDE); конвертировать вручную не нужно.
## Языки
- **TypeScript** — этот клиент (JavaScript/TypeScript);
- **Python / Go** — в планах (тот же скоуп `@apilki`, отдельные репозитории).
## Лицензия
MIT © apilki. Клиент сгенерирован из спецификации Yandex Market Partner API
(https://github.com/yandex-market/yandex-market-partner-api), Copyright (c) 2023
YANDEX LLC, лицензирована по BSD 3-Clause. Полный текст — в [LICENSE](LICENSE).
## Ссылки
- [Официальная документация API Яндекс Маркета для продавцов](https://yandex.ru/dev/market/partner-api/doc/ru/)
- [Спецификация API Яндекс Маркета для продавцов](https://github.com/yandex-market/yandex-market-partner-api)

View File

@@ -1,6 +1,6 @@
{
"name": "@apilki/yandex-market",
"version": "0.1.0",
"version": "0.3.0",
"description": "JavaScript/TypeScript client for Yandex Market Partner API",
"author": "vitya.kuznetsov@gmail.com",
"repository": {
@@ -13,7 +13,8 @@
"sideEffects": false,
"scripts": {
"build": "tsc && tsc -p tsconfig.esm.json",
"prepare": "npm run build"
"prepare": "npm run build",
"test": "node --test"
},
"devDependencies": {
"typescript": "^4.0 || ^5.0"

38
test/api-surface.test.js Executable file
View File

@@ -0,0 +1,38 @@
// Контракт API-поверхности клиента (фидбек books 2026-08-21): импорт класса
// не должен падать на загрузке модуля — ESM «SyntaxError: does not provide an
// export named X» ловится только при instantiate и валит ВЕСЬ пакет. Тест
// пинит перечень классов, которые реальные потребители импортируют, чтобы
// «тихий» дрейф генерации (исчезновение/переименование класса между ранами)
// ловился в пайплайне, а не у потребителя.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { clientDir, hasDist } = require('./client-path');
const skip = hasDist ? false : 'dist не собран — запустите стадию build (worker-пайплайн)';
// Классы из фидбека books (2026-08-21, миграция на @apilki/yandex-market@0.1.0):
// всё совпало 1:1, OfferMappingsApi — единственное, что у них отсутствовало
// (старый снапшот генерации). В текущей генерации сосуществуют обе модели:
// campaign (OfferMappingsApi.getOfferMappingEntries(campaignId,...)) и business
// (BusinessOfferMappingsApi.getOfferMappings(businessId,...)).
// 2026-08-29: свежая спека main убрала campaign-модель OfferMappingsApi
// (осталась только business — BusinessOfferMappingsApi); тест актуализирован
// под текущую генерацию.
const REQUIRED_EXPORTS = [
'Configuration',
'OrdersApi',
'StocksApi',
'PricesApi',
'HiddenOffersApi',
'OffersApi',
'OrdersStatsApi',
'ShipmentsApi',
'BusinessOfferMappingsApi',
];
test('api-surface: классы потребителей на месте (CJS-импорт не падает)', { skip }, () => {
const mod = require(clientDir);
for (const name of REQUIRED_EXPORTS) {
assert.ok(name in mod, `отсутствует экспорт ${name} — потребительский import упадёт на instantiate`);
}
});

16
test/client-path.js Executable file
View File

@@ -0,0 +1,16 @@
// Единый резолвер расположения скомпилированного клиента для юнит-тестов:
// dev-репо: tests/client/*.test.js → ../../output/typescript/dist
// пакет: test/*.test.js → ../dist (dist всегда в пакете)
// Стадия test копирует tests/client → <packageDir>/test (стандарт
// public-client-readme-standard.md, «Тесты в дистрибутиве»).
const fs = require('node:fs');
const path = require('node:path');
const candidates = [
path.resolve(__dirname, '../dist'),
path.resolve(__dirname, '../../output/typescript/dist'),
];
const clientDir = candidates.find((d) => fs.existsSync(path.join(d, 'index.js'))) ?? null;
module.exports = { clientDir, hasDist: Boolean(clientDir) };

110
test/request-shape.test.js Executable file
View File

@@ -0,0 +1,110 @@
// Юнит-набор по ключевым семействам запросов (стандарт public-client-readme-standard.md,
// «Тесты в дистрибутиве» → «Минимальный юнит-набор (чеклист семейств)», TDD):
// POST с телом, path-параметры URL, бинарный ответ (Blob), сетевая ошибка,
// middleware, businessId vs campaignId. Офлайн, без зависимостей, node:test.
// Репрезентанты — из сгенерированного клиента (typescript-fetch):
// getBusinessOrders(businessId, body) — POST /v1/businesses/{businessId}/orders
// getOrder(campaignId, orderId) — GET /v2/campaigns/{campaignId}/orders/{orderId}
// downloadShipmentAct(campaignId, shipmentId) — GET …/shipments/{shipmentId}/act → Blob
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { clientDir, hasDist } = require('./client-path');
const { Configuration, OrdersApi, FbsApi, CampaignsApi, FetchError } = hasDist ? require(clientDir) : {};
const skip = hasDist ? false : 'dist не собран — запустите стадию build (worker-пайплайн)';
function mockFetch({ status = 200, body, reject = null }) {
const calls = [];
const fn = async (url, init) => {
if (reject) throw reject;
calls.push({ url, init });
return new Response(body, { status });
};
fn.calls = calls;
return fn;
}
test('getBusinessOrders: POST с JSON-телом + Api-Key (сериализация body, не только query)', { skip }, async () => {
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ orders: [] }) });
const api = new OrdersApi(new Configuration({ apiKey: 'test-key', fetchApi: fetchMock }));
await api.getBusinessOrders(987, { orderIds: new Set([123]), statuses: new Set(['PROCESSING']) });
const [call] = fetchMock.calls;
assert.equal(call.init.method, 'POST', 'метод POST');
assert.match(call.url, /\/v1\/businesses\/987\/orders$/);
assert.equal(call.init.headers['Api-Key'], 'test-key');
assert.equal(call.init.headers['Content-Type'], 'application/json');
const body = JSON.parse(call.init.body);
assert.deepEqual(body.orderIds, [123], 'Set → массив в теле');
assert.deepEqual(body.statuses, ['PROCESSING']);
});
test('getOrder: path-параметры в URL (/v2/campaigns/123/orders/456), query отсутствует', { skip }, async () => {
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ order: null }) }); // order:null парсится (guard), предмет — URL
const api = new OrdersApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
await api.getOrder(123, 456);
const [call] = fetchMock.calls;
assert.equal(call.init.method, 'GET');
assert.match(call.url, /\/v2\/campaigns\/123\/orders\/456$/);
assert.ok(!call.url.includes('?'), 'нет query — оба id в path');
});
test('downloadShipmentAct: бинарный ответ → Blob (не JSON-парс)', { skip }, async () => {
const fetchMock = mockFetch({ status: 200, body: new Blob(['PDF-CONTENT']) });
const api = new FbsApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
const blob = await api.downloadShipmentAct(123, 456);
assert.ok(blob instanceof Blob, 'ответ — Blob');
assert.equal(await blob.text(), 'PDF-CONTENT');
const [call] = fetchMock.calls;
assert.match(call.url, /\/v2\/campaigns\/123\/first-mile\/shipments\/456\/act$/);
});
test('сетевая ошибка (fetch reject) → FetchError, не тихий success', { skip }, async () => {
const fetchMock = mockFetch({ reject: new TypeError('fetch failed') });
const api = new CampaignsApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
await assert.rejects(() => api.getCampaigns(1), FetchError);
});
test('middleware: pre мутирует запрос (добавляет заголовок), post видит ответ', { skip }, async () => {
const seen = [];
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ campaigns: [] }) });
const api = new CampaignsApi(
new Configuration({
apiKey: 'k',
fetchApi: fetchMock,
middleware: [
{
pre: async ({ url, init }) => ({ url, init: { ...init, headers: { ...init.headers, 'X-Trace': 'abc' } } }),
post: async ({ response }) => {
seen.push(response.status);
return response;
},
},
],
})
);
await api.getCampaigns(1);
const [call] = fetchMock.calls;
assert.equal(call.init.headers['X-Trace'], 'abc', 'pre-заголовок дошёл до fetch');
assert.deepEqual(seen, [200], 'post вызван с ответом');
});
test('businessId vs campaignId: правильный идентификатор уходит в URL', { skip }, async () => {
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ orders: [] }) });
const api = new OrdersApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
await api.getBusinessOrders(987, { orderIds: new Set([1]) }); // businessId-метод
await api.getOrder(123, 456); // campaignId-метод
const [biz, camp] = fetchMock.calls;
assert.match(biz.url, /\/v1\/businesses\/987\/orders$/, 'businessId в URL бизнес-метода');
assert.match(camp.url, /\/v2\/campaigns\/123\/orders\/456$/, 'campaignId в URL кампаний');
});

57
test/response-error.test.js Executable file
View File

@@ -0,0 +1,57 @@
// Юнит-тесты сгенерированного клиента (publish-model.md — обязательный гейт):
// mock fetch через Configuration.fetchApi (typescript-fetch runtime):
// - URL/заголовки запроса (Api-Key из конфига, query-параметры)
// - 2xx → распарсенные данные
// - 4xx/5xx → ResponseError (ответ в err.response)
// Клиент импортируется из скомпилированного dist (после стадии build).
// Расположение dist — через client-path.js (dev-репо и пакет живут на разной глубине).
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { clientDir, hasDist } = require('./client-path');
const { Configuration, CampaignsApi, ResponseError } = hasDist ? require(clientDir) : {};
const skipNoDist = hasDist ? false : 'dist не собран — запустите стадию build (worker-пайплайн)';
function mockFetch({ status, body, headers = {} }) {
const calls = [];
const fn = async (url, init) => {
calls.push({ url, init });
return new Response(body, { status, headers });
};
fn.calls = calls;
return fn;
}
test('getCampaigns: 2xx → данные; запрос несёт Api-Key и query', { skip: skipNoDist }, async () => {
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ campaigns: [{ id: 1, title: 'Магазин' }] }) });
const api = new CampaignsApi(new Configuration({ apiKey: 'test-key', fetchApi: fetchMock }));
const res = await api.getCampaigns('token-1', 10);
assert.equal(res.campaigns.length, 1);
const [call] = fetchMock.calls;
assert.match(call.url, /\/campaigns\?pageToken=token-1&limit=10$/);
assert.equal(call.init.headers['Api-Key'], 'test-key');
});
test('getCampaigns: 4xx → ResponseError, статус и тело доступны', { skip: skipNoDist }, async () => {
const fetchMock = mockFetch({ status: 403, body: JSON.stringify({ code: 'FORBIDDEN', message: 'нет доступа' }) });
const api = new CampaignsApi(new Configuration({ apiKey: 'test-key', fetchApi: fetchMock }));
let caught = null;
try {
await api.getCampaigns(1);
} catch (err) {
caught = err;
}
assert.ok(caught instanceof ResponseError, 'ожидался ResponseError');
assert.equal(caught.response.status, 403);
const body = await caught.response.json();
assert.equal(body.code, 'FORBIDDEN');
});
test('getCampaigns: 5xx → ResponseError', { skip: skipNoDist }, async () => {
const fetchMock = mockFetch({ status: 500, body: 'oops' });
const api = new CampaignsApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
await assert.rejects(() => api.getCampaigns(1), ResponseError);
});