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.
@@ -126,8 +126,15 @@ interface EntityDefinition {
126
126
  * start = end and the record is expired without ever running.
127
127
  */
128
128
  retireStartField?: string;
129
+ /**
130
+ * Header naming child records to delete instead of the whole record
131
+ * (`productComponentInstanceIds: 1,2`). Only `item()` DELETE uses it, and only when
132
+ * the caller's `extras` carry this one key (a non-empty list of ids); every other
133
+ * `extras` key is still dropped. Only for `deleteMode: "delete"`.
134
+ */
135
+ childIdsHeader?: string;
129
136
  }
130
- type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
137
+ type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer" | "productComponent";
131
138
  /**
132
139
  * Addressing per entity, following the Shopwave API reference
133
140
  * (https://developer.merchantstack.com/api-reference.html) and checked against
@@ -140,6 +147,27 @@ type EntityKind = "product" | "category" | "store" | "promotion" | "employee" |
140
147
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
141
148
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
142
149
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
150
+ * - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
151
+ * API reference lists no read filters; these are what the live API does:
152
+ * - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
153
+ * products don't match). `productComponentIds` is ignored on reads: every group comes back.
154
+ * `deleted: false` gives live groups only, `true` deleted groups only, no header both.
155
+ * No match → 204 with an empty body. No order, sort, priority or position field anywhere.
156
+ * - Groups are keyed by id; `products` (option products) by product id; `productInstances`
157
+ * (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
158
+ * vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
159
+ * a `tags` array is joined with commas; `tags` null or left out → null).
160
+ * - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
161
+ * are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
162
+ * on a price change) and rows left out are removed. Option products left out of the save
163
+ * are untouched, and an empty `productInstances` removes nothing. A save without `products`
164
+ * updates only the group's own fields. The echo isn't proof of what was stored: renames of
165
+ * groups created moments earlier by a test run echoed the new name but never stuck (re-read
166
+ * for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
167
+ * once. Cause not yet known.
168
+ * - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
169
+ * `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
170
+ * `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
143
171
  */
144
172
  declare const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>;
145
173
  //#endregion
@@ -303,7 +331,10 @@ interface ItemHandlers {
303
331
  GET: RouteHandler;
304
332
  /** Update the record at `[id]` (the id in the URL wins). */
305
333
  PUT: RouteHandler;
306
- /** Delete the record at `[id]`. Shopwave answers 205 with an empty body. */
334
+ /**
335
+ * Delete the record at `[id]`. Shopwave answers 205 with an empty body. For an entity with
336
+ * `childIdsHeader`, `extras` carrying that key deletes just those child rows instead.
337
+ */
307
338
  DELETE: RouteHandler;
308
339
  }
309
340
  interface ResourceHandlers {
@@ -126,8 +126,15 @@ interface EntityDefinition {
126
126
  * start = end and the record is expired without ever running.
127
127
  */
128
128
  retireStartField?: string;
129
+ /**
130
+ * Header naming child records to delete instead of the whole record
131
+ * (`productComponentInstanceIds: 1,2`). Only `item()` DELETE uses it, and only when
132
+ * the caller's `extras` carry this one key (a non-empty list of ids); every other
133
+ * `extras` key is still dropped. Only for `deleteMode: "delete"`.
134
+ */
135
+ childIdsHeader?: string;
129
136
  }
130
- type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
137
+ type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer" | "productComponent";
131
138
  /**
132
139
  * Addressing per entity, following the Shopwave API reference
133
140
  * (https://developer.merchantstack.com/api-reference.html) and checked against
@@ -140,6 +147,27 @@ type EntityKind = "product" | "category" | "store" | "promotion" | "employee" |
140
147
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
141
148
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
142
149
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
150
+ * - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
151
+ * API reference lists no read filters; these are what the live API does:
152
+ * - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
153
+ * products don't match). `productComponentIds` is ignored on reads: every group comes back.
154
+ * `deleted: false` gives live groups only, `true` deleted groups only, no header both.
155
+ * No match → 204 with an empty body. No order, sort, priority or position field anywhere.
156
+ * - Groups are keyed by id; `products` (option products) by product id; `productInstances`
157
+ * (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
158
+ * vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
159
+ * a `tags` array is joined with commas; `tags` null or left out → null).
160
+ * - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
161
+ * are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
162
+ * on a price change) and rows left out are removed. Option products left out of the save
163
+ * are untouched, and an empty `productInstances` removes nothing. A save without `products`
164
+ * updates only the group's own fields. The echo isn't proof of what was stored: renames of
165
+ * groups created moments earlier by a test run echoed the new name but never stuck (re-read
166
+ * for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
167
+ * once. Cause not yet known.
168
+ * - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
169
+ * `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
170
+ * `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
143
171
  */
144
172
  declare const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>;
145
173
  //#endregion
@@ -303,7 +331,10 @@ interface ItemHandlers {
303
331
  GET: RouteHandler;
304
332
  /** Update the record at `[id]` (the id in the URL wins). */
305
333
  PUT: RouteHandler;
306
- /** Delete the record at `[id]`. Shopwave answers 205 with an empty body. */
334
+ /**
335
+ * Delete the record at `[id]`. Shopwave answers 205 with an empty body. For an entity with
336
+ * `childIdsHeader`, `extras` carrying that key deletes just those child rows instead.
337
+ */
307
338
  DELETE: RouteHandler;
308
339
  }
309
340
  interface ResourceHandlers {
@@ -330,6 +330,27 @@ const SHOPWAVE_LOOKUPS = Object.freeze({
330
330
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
331
331
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
332
332
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
333
+ * - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
334
+ * API reference lists no read filters; these are what the live API does:
335
+ * - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
336
+ * products don't match). `productComponentIds` is ignored on reads: every group comes back.
337
+ * `deleted: false` gives live groups only, `true` deleted groups only, no header both.
338
+ * No match → 204 with an empty body. No order, sort, priority or position field anywhere.
339
+ * - Groups are keyed by id; `products` (option products) by product id; `productInstances`
340
+ * (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
341
+ * vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
342
+ * a `tags` array is joined with commas; `tags` null or left out → null).
343
+ * - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
344
+ * are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
345
+ * on a price change) and rows left out are removed. Option products left out of the save
346
+ * are untouched, and an empty `productInstances` removes nothing. A save without `products`
347
+ * updates only the group's own fields. The echo isn't proof of what was stored: renames of
348
+ * groups created moments earlier by a test run echoed the new name but never stuck (re-read
349
+ * for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
350
+ * once. Cause not yet known.
351
+ * - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
352
+ * `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
353
+ * `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
333
354
  */
334
355
  const SHOPWAVE_ENTITIES = Object.freeze({
335
356
  product: {
@@ -389,6 +410,16 @@ const SHOPWAVE_ENTITIES = Object.freeze({
389
410
  idHeader: "consumerId",
390
411
  writable: false,
391
412
  deleteMode: "none"
413
+ },
414
+ productComponent: {
415
+ route: "product-components",
416
+ collection: "productComponents",
417
+ upstream: "product/component",
418
+ idsHeader: "componentIds",
419
+ idHeader: "productComponentIds",
420
+ writable: true,
421
+ deleteMode: "delete",
422
+ childIdsHeader: "productComponentInstanceIds"
392
423
  }
393
424
  });
394
425
 
@@ -562,6 +593,29 @@ async function readUpstream(response) {
562
593
  };
563
594
  }
564
595
  }
596
+ /**
597
+ * The child ids a DELETE names in `extras` under `header` (case-insensitive), as one comma
598
+ * list: `undefined` when the key is absent, `null` when `extras` or the value is invalid (not a
599
+ * JSON object, or not a non-empty list of ids). The key may hold an array or a comma string.
600
+ */
601
+ function readChildIds(raw, header) {
602
+ if (!raw) return void 0;
603
+ let parsed;
604
+ try {
605
+ parsed = JSON.parse(raw);
606
+ } catch {
607
+ return null;
608
+ }
609
+ if (!isPlainObject(parsed)) return null;
610
+ const key = Object.keys(parsed).find((k) => k.toLowerCase() === header.toLowerCase());
611
+ if (key === void 0) return void 0;
612
+ const value = parsed[key];
613
+ const list = Array.isArray(value) ? value : typeof value === "string" || typeof value === "number" ? String(value).split(",") : null;
614
+ if (!list) return null;
615
+ const ids = list.map((v) => typeof v === "string" || typeof v === "number" ? String(v).trim() : "");
616
+ if (ids.length === 0 || ids.some((id) => !ID_PATTERN.test(id))) return null;
617
+ return ids.join(",");
618
+ }
565
619
  async function routeParam(context, name) {
566
620
  const value = (await context?.params)?.[name];
567
621
  const id = Array.isArray(value) ? value[0] : value;
@@ -812,6 +866,19 @@ function createShopwaveApiHandlers(config) {
812
866
  const id = await routeParam(context, param);
813
867
  if (!id) return badId();
814
868
  if (def.deleteMode === "none") return notSupported(def, "deletes");
869
+ if (def.deleteMode === "delete" && def.childIdsHeader) {
870
+ const childIds = readChildIds(request.headers.get("extras"), def.childIdsHeader);
871
+ if (childIds === null) return jsonError(400, "invalid_extras", `"${def.childIdsHeader}" must be a non-empty list of ids`);
872
+ if (childIds !== void 0) return forward(request, {
873
+ method: "DELETE",
874
+ path: def.upstream,
875
+ headers: {
876
+ [def.idHeader]: id,
877
+ [def.childIdsHeader]: childIds
878
+ },
879
+ forwardExtras: false
880
+ });
881
+ }
815
882
  if (def.deleteMode === "retire") {
816
883
  const field = def.retireField ?? "exitDate";
817
884
  if (def.retireWithRecord) return retireWithRecord(request, def, id, field);