@viu/emporix-sdk 1.0.0 → 2.1.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +26 -1
  3. package/dist/availability.cjs +101 -0
  4. package/dist/availability.cjs.map +1 -0
  5. package/dist/availability.d.cts +87 -0
  6. package/dist/availability.d.ts +87 -0
  7. package/dist/availability.js +4 -0
  8. package/dist/availability.js.map +1 -0
  9. package/dist/cart.d.cts +1 -1
  10. package/dist/cart.d.ts +1 -1
  11. package/dist/category.d.cts +1 -1
  12. package/dist/category.d.ts +1 -1
  13. package/dist/checkout.d.cts +1 -1
  14. package/dist/checkout.d.ts +1 -1
  15. package/dist/{chunk-QDABAMVN.js → chunk-JTJV7WUW.js} +25 -2
  16. package/dist/chunk-JTJV7WUW.js.map +1 -0
  17. package/dist/chunk-SCI7D74B.js +91 -0
  18. package/dist/chunk-SCI7D74B.js.map +1 -0
  19. package/dist/chunk-WSXRUYBE.js +248 -0
  20. package/dist/chunk-WSXRUYBE.js.map +1 -0
  21. package/dist/chunk-XMEP74BU.js +60 -0
  22. package/dist/chunk-XMEP74BU.js.map +1 -0
  23. package/dist/companies.d.cts +1 -1
  24. package/dist/companies.d.ts +1 -1
  25. package/dist/contacts.d.cts +1 -1
  26. package/dist/contacts.d.ts +1 -1
  27. package/dist/{context-vKgzZ-5f.d.cts → context-CGuxSb56.d.cts} +16 -1
  28. package/dist/{context-vKgzZ-5f.d.ts → context-CGuxSb56.d.ts} +16 -1
  29. package/dist/customer-groups.d.cts +1 -1
  30. package/dist/customer-groups.d.ts +1 -1
  31. package/dist/customer.d.cts +1 -1
  32. package/dist/customer.d.ts +1 -1
  33. package/dist/index.cjs +311 -9
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +8 -5
  36. package/dist/index.d.ts +8 -5
  37. package/dist/index.js +61 -6
  38. package/dist/index.js.map +1 -1
  39. package/dist/locations.d.cts +1 -1
  40. package/dist/locations.d.ts +1 -1
  41. package/dist/media.cjs +218 -9
  42. package/dist/media.cjs.map +1 -1
  43. package/dist/media.d.cts +103 -7
  44. package/dist/media.d.ts +103 -7
  45. package/dist/media.js +2 -1
  46. package/dist/orders.d.cts +1 -1
  47. package/dist/orders.d.ts +1 -1
  48. package/dist/payment.d.cts +1 -1
  49. package/dist/payment.d.ts +1 -1
  50. package/dist/price.cjs +44 -0
  51. package/dist/price.cjs.map +1 -1
  52. package/dist/price.d.cts +27 -2
  53. package/dist/price.d.ts +27 -2
  54. package/dist/price.js +1 -1
  55. package/dist/product.cjs +23 -0
  56. package/dist/product.cjs.map +1 -1
  57. package/dist/product.d.cts +18 -1
  58. package/dist/product.d.ts +18 -1
  59. package/dist/product.js +1 -1
  60. package/dist/segment.d.cts +1 -1
  61. package/dist/segment.d.ts +1 -1
  62. package/package.json +7 -2
  63. package/dist/chunk-F3WAQH2E.js +0 -120
  64. package/dist/chunk-F3WAQH2E.js.map +0 -1
  65. package/dist/chunk-HEO34WLO.js +0 -47
  66. package/dist/chunk-HEO34WLO.js.map +0 -1
  67. package/dist/chunk-QDABAMVN.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -634,6 +634,57 @@ var HttpClient = class {
634
634
  throw errorFromResponse(res.status, `${o.method} ${o.path} \u2192 ${res.status}`, parsed);
635
635
  }
636
636
  }
637
+ /**
638
+ * Single-shot fetch that returns the raw {@link Response} unparsed. Used by
639
+ * endpoints whose responses are not JSON (binary downloads, redirect-only
640
+ * endpoints). Skips the retry-on-5xx and 401-reauth-once paths intentionally
641
+ * — those depend on parsing the JSON error body, which a raw consumer must
642
+ * handle itself if needed. Auth resolution + timeout + logging are still
643
+ * applied.
644
+ *
645
+ * `extra.redirect` is forwarded to `fetch` so callers can use `'manual'` to
646
+ * capture 30x responses with their `Location` header (Node fetch exposes
647
+ * the header in this mode; browser fetch returns an opaque response).
648
+ */
649
+ async requestRaw(o, extra) {
650
+ const requestId = `req-${++requestSeq}`;
651
+ const log = this.opts.logger.child({ requestId });
652
+ const url = new URL(this.opts.host + o.path);
653
+ for (const [k, v] of Object.entries(o.query ?? {})) {
654
+ if (v !== void 0) url.searchParams.set(k, String(v));
655
+ }
656
+ const token = await resolveToken(o.auth, this.opts.provider);
657
+ log.debug("http requestRaw", {
658
+ authKind: o.auth.kind,
659
+ method: o.method,
660
+ url: url.pathname,
661
+ redirect: extra?.redirect ?? "follow"
662
+ });
663
+ const controller = new AbortController();
664
+ const timer = setTimeout(
665
+ () => controller.abort(),
666
+ o.timeoutMs ?? this.opts.timeouts.readMs
667
+ );
668
+ const isFormData = typeof FormData !== "undefined" && o.body instanceof FormData;
669
+ const init = {
670
+ method: o.method,
671
+ headers: {
672
+ ...o.headers ?? {},
673
+ Authorization: `Bearer ${token}`,
674
+ ...o.body !== void 0 && !isFormData ? { "Content-Type": "application/json" } : {}
675
+ },
676
+ signal: controller.signal,
677
+ ...extra?.redirect ? { redirect: extra.redirect } : {}
678
+ };
679
+ if (o.body !== void 0) {
680
+ init.body = isFormData ? o.body : JSON.stringify(o.body);
681
+ }
682
+ try {
683
+ return await fetch(url, init);
684
+ } finally {
685
+ clearTimeout(timer);
686
+ }
687
+ }
637
688
  };
638
689
 
639
690
  // src/core/require-customer.ts
@@ -945,6 +996,29 @@ var ProductService = class {
945
996
  );
946
997
  return pages.flat();
947
998
  }
999
+ /**
1000
+ * Streams the VARIANT children of a PARENT_VARIANT product, page by page,
1001
+ * via the search query `productType:VARIANT parentVariantId:<id>`. Default
1002
+ * pageSize 200. The query syntax (space-separated fields = implicit AND) is
1003
+ * encapsulated here so consumers don't build it themselves.
1004
+ */
1005
+ listVariantChildrenAll(parentVariantId, params = {}, auth2 = ANON) {
1006
+ const pageSize = params.pageSize ?? 200;
1007
+ const q = `productType:VARIANT parentVariantId:${parentVariantId}`;
1008
+ return iterateAll((pageNumber) => this.search(q, { pageNumber, pageSize }, auth2));
1009
+ }
1010
+ /**
1011
+ * Resolves ALL VARIANT children of a PARENT_VARIANT product into a flat
1012
+ * array (loads every page). Default pageSize 200. Returns `[]` when there are
1013
+ * no children — never throws.
1014
+ */
1015
+ async listVariantChildren(parentVariantId, params = {}, auth2 = ANON) {
1016
+ const out = [];
1017
+ for await (const child of this.listVariantChildrenAll(parentVariantId, params, auth2)) {
1018
+ out.push(child);
1019
+ }
1020
+ return out;
1021
+ }
948
1022
  };
949
1023
 
950
1024
  // src/services/category.ts
@@ -1311,6 +1385,50 @@ var PriceService = class {
1311
1385
  body: input
1312
1386
  });
1313
1387
  }
1388
+ /**
1389
+ * Chunked variant of {@link matchByContext} for large `items` arrays. The
1390
+ * Emporix backend handles only a limited number of items per request
1391
+ * (production limit ~50), so this splits `input.items` into chunks and runs
1392
+ * `matchByContext` with bounded concurrency.
1393
+ *
1394
+ * By default a failing chunk is skipped (its items are absent from the
1395
+ * result) and `opts.onChunkError` is called once for it; pass
1396
+ * `throwOnAnyChunkError: true` to reject on the first failure instead.
1397
+ *
1398
+ * **Result order is not guaranteed** — match entries back to your items by
1399
+ * `priceId` / `itemRef.id`.
1400
+ */
1401
+ async matchByContextChunked(input, opts = {}, auth2) {
1402
+ const chunkSize = opts.chunkSize ?? 50;
1403
+ const concurrency = opts.concurrency ?? 4;
1404
+ if (chunkSize < 1) throw new Error("chunkSize must be >= 1");
1405
+ if (concurrency < 1) throw new Error("concurrency must be >= 1");
1406
+ const items = input.items ?? [];
1407
+ if (items.length === 0) return [];
1408
+ const chunks = [];
1409
+ for (let i = 0; i < items.length; i += chunkSize) {
1410
+ chunks.push({ ...input, items: items.slice(i, i + chunkSize) });
1411
+ }
1412
+ const results = new Array(chunks.length);
1413
+ let cursor = 0;
1414
+ const worker = async () => {
1415
+ for (; ; ) {
1416
+ const idx = cursor++;
1417
+ const chunk = chunks[idx];
1418
+ if (chunk === void 0) return;
1419
+ try {
1420
+ results[idx] = await this.matchByContext(chunk, auth2);
1421
+ } catch (err) {
1422
+ if (opts.throwOnAnyChunkError) throw err;
1423
+ results[idx] = [];
1424
+ opts.onChunkError?.(err, idx);
1425
+ }
1426
+ }
1427
+ };
1428
+ const workerCount = Math.min(concurrency, chunks.length);
1429
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
1430
+ return results.flat();
1431
+ }
1314
1432
  };
1315
1433
 
1316
1434
  // src/services/media.ts
@@ -1346,14 +1464,29 @@ var MediaService = class {
1346
1464
  body: input.body
1347
1465
  });
1348
1466
  }
1349
- /** List assets. Optional `query` is passed through to Emporix verbatim. */
1350
- async list(query, auth2 = SERVICE2) {
1351
- return this.ctx.http.request({
1467
+ /**
1468
+ * List assets, wrapped in the shared {@link PaginatedItems} envelope so
1469
+ * callers can iterate over pages consistently with the rest of the SDK.
1470
+ * `hasNextPage` is the standard SDK heuristic: `true` when the returned
1471
+ * page is full (`items.length === requested pageSize`). Pagination
1472
+ * defaults match Emporix's server defaults (`pageNumber: 1`,
1473
+ * `pageSize: 60`) so an empty `query` produces stable expectations.
1474
+ */
1475
+ async list(query = {}, auth2 = SERVICE2) {
1476
+ const pageNumber = query.pageNumber ?? 1;
1477
+ const pageSize = query.pageSize ?? 60;
1478
+ const items = await this.ctx.http.request({
1352
1479
  method: "GET",
1353
1480
  path: this.base(),
1354
1481
  auth: auth2,
1355
- ...query ? { query } : {}
1482
+ query: { ...query, pageNumber, pageSize }
1356
1483
  });
1484
+ return {
1485
+ items,
1486
+ pageNumber,
1487
+ pageSize,
1488
+ hasNextPage: items.length === pageSize
1489
+ };
1357
1490
  }
1358
1491
  /** Fetch an asset by id. */
1359
1492
  async get(assetId, auth2 = SERVICE2) {
@@ -1363,13 +1496,37 @@ var MediaService = class {
1363
1496
  auth: auth2
1364
1497
  });
1365
1498
  }
1366
- /** Update an asset (e.g. swap `refIds` or `access`). */
1367
- async update(assetId, patch, auth2 = SERVICE2) {
1499
+ /**
1500
+ * Update an asset. The input is a discriminated union mirroring
1501
+ * {@link create}:
1502
+ * - `{ kind: "json", body }` — metadata-only patch (BLOB or LINK).
1503
+ * Sends `application/json`. Used for `refIds`, `details`, `metadata`,
1504
+ * or `url` changes.
1505
+ * - `{ kind: "blob", file, body }` — replaces the BLOB file content
1506
+ * (max 10MB) AND patches metadata in the same request. Sends
1507
+ * `multipart/form-data`.
1508
+ *
1509
+ * `type` and `access` are immutable per Emporix — they must match the
1510
+ * existing asset's values. Optimistic-locking is via `body.metadata.version`;
1511
+ * the server returns 409 Conflict on a stale version.
1512
+ */
1513
+ async update(assetId, input, auth2 = SERVICE2) {
1514
+ if (input.kind === "blob") {
1515
+ const fd = new FormData();
1516
+ fd.set("file", input.file);
1517
+ fd.set("body", JSON.stringify(input.body));
1518
+ return this.ctx.http.request({
1519
+ method: "PUT",
1520
+ path: `${this.base()}/${assetId}`,
1521
+ auth: auth2,
1522
+ body: fd
1523
+ });
1524
+ }
1368
1525
  return this.ctx.http.request({
1369
1526
  method: "PUT",
1370
1527
  path: `${this.base()}/${assetId}`,
1371
1528
  auth: auth2,
1372
- body: patch
1529
+ body: input.body
1373
1530
  });
1374
1531
  }
1375
1532
  /** Remove an asset. */
@@ -1380,6 +1537,72 @@ var MediaService = class {
1380
1537
  auth: auth2
1381
1538
  });
1382
1539
  }
1540
+ /**
1541
+ * Download an asset by id. Returns a discriminated union:
1542
+ * - `{ kind: "redirect", url }` for `PUBLIC` assets (server-side 30x with
1543
+ * the storage URL in `Location`).
1544
+ * - `{ kind: "bytes", data }` for `PRIVATE` assets (server-side 200 with
1545
+ * the asset bytes; `etag` + `contentType` headers are exposed for
1546
+ * caching). When the server reports `text/plain` Content-Type (the
1547
+ * OpenAPI-documented format), the SDK decodes the base64 stream into
1548
+ * an `ArrayBuffer` transparently; binary Content-Types are passed
1549
+ * through verbatim.
1550
+ *
1551
+ * Implementation note: uses `redirect: "manual"` so the redirect-location
1552
+ * is observable. In Node.js this works; in a browser the redirect Location
1553
+ * is intentionally hidden by fetch — there, `PUBLIC` downloads will
1554
+ * surface as an opaque-redirect and the SDK throws. Browser code should
1555
+ * use the asset's `url` field from `get()` for `LINK` assets, or the
1556
+ * direct storage URL for `PUBLIC` `BLOB` assets (typically delivered via
1557
+ * an `<img>` tag rather than `download()`).
1558
+ */
1559
+ async download(assetId, auth2 = SERVICE2) {
1560
+ const path = `${this.base()}/${assetId}/download`;
1561
+ const res = await this.ctx.http.requestRaw(
1562
+ { method: "GET", path, auth: auth2 },
1563
+ { redirect: "manual" }
1564
+ );
1565
+ if (res.status >= 300 && res.status < 400) {
1566
+ const url = res.headers.get("location");
1567
+ if (!url) {
1568
+ throw new EmporixError(
1569
+ `media.download: ${res.status} response without a Location header`,
1570
+ res.status
1571
+ );
1572
+ }
1573
+ return { kind: "redirect", url };
1574
+ }
1575
+ if (res.ok) {
1576
+ const etag = res.headers.get("etag") ?? void 0;
1577
+ const contentType = res.headers.get("content-type") ?? void 0;
1578
+ let data;
1579
+ if (contentType?.startsWith("text/plain")) {
1580
+ const text2 = await res.text();
1581
+ const bin = atob(text2);
1582
+ const arr = new Uint8Array(bin.length);
1583
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
1584
+ data = arr.buffer;
1585
+ } else {
1586
+ data = await res.arrayBuffer();
1587
+ }
1588
+ return {
1589
+ kind: "bytes",
1590
+ data,
1591
+ ...etag !== void 0 ? { etag } : {},
1592
+ ...contentType !== void 0 ? { contentType } : {}
1593
+ };
1594
+ }
1595
+ const text = await res.text();
1596
+ let parsed = void 0;
1597
+ if (text) {
1598
+ try {
1599
+ parsed = JSON.parse(text);
1600
+ } catch {
1601
+ parsed = text;
1602
+ }
1603
+ }
1604
+ throw errorFromResponse(res.status, `GET ${path} \u2192 ${res.status}`, parsed);
1605
+ }
1383
1606
  /** Multipart upload sugar: builds `AssetCreateBlob` from input. */
1384
1607
  async uploadFile(input, auth2 = SERVICE2) {
1385
1608
  const body = {
@@ -1395,6 +1618,27 @@ var MediaService = class {
1395
1618
  };
1396
1619
  return this.create({ kind: "blob", file: input.file, body }, auth2);
1397
1620
  }
1621
+ /**
1622
+ * Replace the file content of an existing BLOB asset. Sugar over
1623
+ * {@link update} with `kind: "blob"`. `access` is required because the
1624
+ * field is immutable and the server validates that the patch matches.
1625
+ * Pass `version` from `asset.metadata.version` if you want optimistic
1626
+ * locking (recommended when concurrent writers are possible).
1627
+ */
1628
+ async replaceFile(assetId, input, auth2 = SERVICE2) {
1629
+ const body = {
1630
+ type: "BLOB",
1631
+ access: input.access,
1632
+ ...input.filename || input.mimeType ? {
1633
+ details: {
1634
+ ...input.filename ? { filename: input.filename } : {},
1635
+ ...input.mimeType ? { mimeType: input.mimeType } : {}
1636
+ }
1637
+ } : {},
1638
+ ...input.version !== void 0 ? { metadata: { version: input.version } } : {}
1639
+ };
1640
+ return this.update(assetId, { kind: "blob", file: input.file, body }, auth2);
1641
+ }
1398
1642
  /** External-URL sugar: builds `AssetCreateLink`. */
1399
1643
  async link(input, auth2 = SERVICE2) {
1400
1644
  const body = {
@@ -1412,7 +1656,7 @@ var MediaService = class {
1412
1656
  if (refIds.some((r) => isProductRef(r, productId))) return a;
1413
1657
  const next = [...refIds, { type: "PRODUCT", id: productId }];
1414
1658
  const patch = { type: a.type, refIds: next };
1415
- return this.update(assetId, patch, auth2);
1659
+ return this.update(assetId, { kind: "json", body: patch }, auth2);
1416
1660
  }
1417
1661
  /** Remove a PRODUCT refId from an asset (no-op if absent). */
1418
1662
  async detachFromProduct(assetId, productId, auth2 = SERVICE2) {
@@ -1421,7 +1665,7 @@ var MediaService = class {
1421
1665
  const next = refIds.filter((r) => !isProductRef(r, productId));
1422
1666
  if (next.length === refIds.length) return a;
1423
1667
  const patch = { type: a.type, refIds: next };
1424
- return this.update(assetId, patch, auth2);
1668
+ return this.update(assetId, { kind: "json", body: patch }, auth2);
1425
1669
  }
1426
1670
  /** Convenience: list assets attached to a product (server-side filter). */
1427
1671
  async listForProduct(productId, auth2 = SERVICE2) {
@@ -1933,6 +2177,61 @@ var SalesOrdersService = class {
1933
2177
  }
1934
2178
  };
1935
2179
 
2180
+ // src/services/availability.ts
2181
+ var ANON6 = { kind: "anonymous" };
2182
+ var AvailabilityService = class {
2183
+ constructor(ctx) {
2184
+ this.ctx = ctx;
2185
+ }
2186
+ ctx;
2187
+ /**
2188
+ * Single product. Resolves the availability record, or — when
2189
+ * `opts.defaultAvailableOnNotFound` is set — a default available record on 404.
2190
+ */
2191
+ async get(productId, siteCode, auth2 = ANON6, opts = {}) {
2192
+ try {
2193
+ return await this.ctx.http.request({
2194
+ method: "GET",
2195
+ path: `/availability/${this.ctx.tenant}/availability/${encodeURIComponent(
2196
+ productId
2197
+ )}/${encodeURIComponent(siteCode)}`,
2198
+ auth: auth2
2199
+ });
2200
+ } catch (err) {
2201
+ if (err instanceof EmporixNotFoundError && opts.defaultAvailableOnNotFound) {
2202
+ return { productId, site: siteCode, available: true };
2203
+ }
2204
+ throw err;
2205
+ }
2206
+ }
2207
+ /**
2208
+ * Batch read via `POST .../availability/search` (one request). Products with
2209
+ * no availability record are absent from the response; each is synthesized as
2210
+ * `{ available: false }` (or `{ available: true }` when
2211
+ * `opts.defaultAvailableOnNotFound` is set). The result preserves input order
2212
+ * and length. An empty `productIds` resolves to `[]` without a request.
2213
+ */
2214
+ async getMany(productIds, siteCode, auth2 = ANON6, opts = {}) {
2215
+ if (productIds.length === 0) return [];
2216
+ const found = await this.ctx.http.request({
2217
+ method: "POST",
2218
+ path: `/availability/${this.ctx.tenant}/availability/search`,
2219
+ auth: auth2,
2220
+ query: { site: siteCode, pageSize: productIds.length },
2221
+ body: productIds
2222
+ });
2223
+ const byId = /* @__PURE__ */ new Map();
2224
+ for (const a of found) if (a.productId) byId.set(a.productId, a);
2225
+ return productIds.map(
2226
+ (id) => byId.get(id) ?? {
2227
+ productId: id,
2228
+ site: siteCode,
2229
+ available: Boolean(opts.defaultAvailableOnNotFound)
2230
+ }
2231
+ );
2232
+ }
2233
+ };
2234
+
1936
2235
  // src/client.ts
1937
2236
  var SDK_VERSION = "0.0.0";
1938
2237
  var EmporixClient = class {
@@ -1953,6 +2252,7 @@ var EmporixClient = class {
1953
2252
  customerGroups;
1954
2253
  orders;
1955
2254
  salesOrders;
2255
+ availability;
1956
2256
  /** The validated tenant this client is bound to. */
1957
2257
  tenant;
1958
2258
  /**
@@ -2022,6 +2322,7 @@ var EmporixClient = class {
2022
2322
  this.customerGroups = new CustomerGroupsService(mk("iam"));
2023
2323
  this.orders = new OrdersService(mk("orders"));
2024
2324
  this.salesOrders = new SalesOrdersService(mk("sales-orders"));
2325
+ this.availability = new AvailabilityService(mk("availability"));
2025
2326
  }
2026
2327
  /** Sets the runtime log level globally or for one service. */
2027
2328
  setLogLevel(level, opts = {}) {
@@ -2033,6 +2334,7 @@ var EmporixClient = class {
2033
2334
  }
2034
2335
  };
2035
2336
 
2337
+ exports.AvailabilityService = AvailabilityService;
2036
2338
  exports.CartService = CartService;
2037
2339
  exports.CategoryService = CategoryService;
2038
2340
  exports.CheckoutService = CheckoutService;