react-shopwave-connect 0.4.6 → 0.4.7

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.
@@ -356,6 +356,27 @@ const SHOPWAVE_LOOKUPS = Object.freeze({
356
356
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
357
357
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
358
358
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
359
+ * - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
360
+ * API reference lists no read filters; these are what the live API does:
361
+ * - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
362
+ * products don't match). `productComponentIds` is ignored on reads: every group comes back.
363
+ * `deleted: false` gives live groups only, `true` deleted groups only, no header both.
364
+ * No match → 204 with an empty body. No order, sort, priority or position field anywhere.
365
+ * - Groups are keyed by id; `products` (option products) by product id; `productInstances`
366
+ * (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
367
+ * vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
368
+ * a `tags` array is joined with commas; `tags` null or left out → null).
369
+ * - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
370
+ * are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
371
+ * on a price change) and rows left out are removed. Option products left out of the save
372
+ * are untouched, and an empty `productInstances` removes nothing. A save without `products`
373
+ * updates only the group's own fields. The echo isn't proof of what was stored: renames of
374
+ * groups created moments earlier by a test run echoed the new name but never stuck (re-read
375
+ * for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
376
+ * once. Cause not yet known.
377
+ * - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
378
+ * `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
379
+ * `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
359
380
  */
360
381
  const SHOPWAVE_ENTITIES = Object.freeze({
361
382
  product: {
@@ -415,6 +436,16 @@ const SHOPWAVE_ENTITIES = Object.freeze({
415
436
  idHeader: "consumerId",
416
437
  writable: false,
417
438
  deleteMode: "none"
439
+ },
440
+ productComponent: {
441
+ route: "product-components",
442
+ collection: "productComponents",
443
+ upstream: "product/component",
444
+ idsHeader: "componentIds",
445
+ idHeader: "productComponentIds",
446
+ writable: true,
447
+ deleteMode: "delete",
448
+ childIdsHeader: "productComponentInstanceIds"
418
449
  }
419
450
  });
420
451
 
@@ -588,6 +619,29 @@ async function readUpstream(response) {
588
619
  };
589
620
  }
590
621
  }
622
+ /**
623
+ * The child ids a DELETE names in `extras` under `header` (case-insensitive), as one comma
624
+ * list: `undefined` when the key is absent, `null` when `extras` or the value is invalid (not a
625
+ * JSON object, or not a non-empty list of ids). The key may hold an array or a comma string.
626
+ */
627
+ function readChildIds(raw, header) {
628
+ if (!raw) return void 0;
629
+ let parsed;
630
+ try {
631
+ parsed = JSON.parse(raw);
632
+ } catch {
633
+ return null;
634
+ }
635
+ if (!isPlainObject(parsed)) return null;
636
+ const key = Object.keys(parsed).find((k) => k.toLowerCase() === header.toLowerCase());
637
+ if (key === void 0) return void 0;
638
+ const value = parsed[key];
639
+ const list = Array.isArray(value) ? value : typeof value === "string" || typeof value === "number" ? String(value).split(",") : null;
640
+ if (!list) return null;
641
+ const ids = list.map((v) => typeof v === "string" || typeof v === "number" ? String(v).trim() : "");
642
+ if (ids.length === 0 || ids.some((id) => !ID_PATTERN.test(id))) return null;
643
+ return ids.join(",");
644
+ }
591
645
  async function routeParam(context, name) {
592
646
  const value = (await context?.params)?.[name];
593
647
  const id = Array.isArray(value) ? value[0] : value;
@@ -838,6 +892,19 @@ function createShopwaveApiHandlers(config) {
838
892
  const id = await routeParam(context, param);
839
893
  if (!id) return badId();
840
894
  if (def.deleteMode === "none") return notSupported(def, "deletes");
895
+ if (def.deleteMode === "delete" && def.childIdsHeader) {
896
+ const childIds = readChildIds(request.headers.get("extras"), def.childIdsHeader);
897
+ if (childIds === null) return jsonError(400, "invalid_extras", `"${def.childIdsHeader}" must be a non-empty list of ids`);
898
+ if (childIds !== void 0) return forward(request, {
899
+ method: "DELETE",
900
+ path: def.upstream,
901
+ headers: {
902
+ [def.idHeader]: id,
903
+ [def.childIdsHeader]: childIds
904
+ },
905
+ forwardExtras: false
906
+ });
907
+ }
841
908
  if (def.deleteMode === "retire") {
842
909
  const field = def.retireField ?? "exitDate";
843
910
  if (def.retireWithRecord) return retireWithRecord(request, def, id, field);