react-shopwave-connect 0.4.5 → 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
@@ -185,8 +213,8 @@ declare const UPLOAD_KINDS: Readonly<Record<UploadKind, string>>;
185
213
  /**
186
214
  * The role an image plays, sent to Shopwave as the uploader's `prefix` header.
187
215
  * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with
188
- * the file wherever a record stores it (a product's `images`). Live, merchant
189
- * 5644, 4 Nov 2026: all four values answered 201.
216
+ * the file wherever a record stores it (a product's `images`). Live, test
217
+ * merchant, 4 Nov 2026: all four values answered 201.
190
218
  * - `product`: a product gallery image;
191
219
  * - `product_thumbnail`: the product's lead image, shown in lists;
192
220
  * - `variant` / `variant_thumbnail`: a variant's own image and its lead image
@@ -208,7 +236,7 @@ declare const UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix>;
208
236
  * on the merchant's products. No parameters. The API reference says
209
237
  * "internal SKU tags are excluded", but the live API includes system tags
210
238
  * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;
211
- * test merchant 5644, 29 Oct 2026). Apps that offer the list as suggestions
239
+ * a test merchant, 29 Oct 2026). Apps that offer the list as suggestions
212
240
  * filter those themselves. Tags of deleted products drop out of the list.
213
241
  * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,
214
242
  * the same for price instances (channels such as "Eat In", "Takeout").
@@ -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
@@ -185,8 +213,8 @@ declare const UPLOAD_KINDS: Readonly<Record<UploadKind, string>>;
185
213
  /**
186
214
  * The role an image plays, sent to Shopwave as the uploader's `prefix` header.
187
215
  * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with
188
- * the file wherever a record stores it (a product's `images`). Live, merchant
189
- * 5644, 4 Nov 2026: all four values answered 201.
216
+ * the file wherever a record stores it (a product's `images`). Live, test
217
+ * merchant, 4 Nov 2026: all four values answered 201.
190
218
  * - `product`: a product gallery image;
191
219
  * - `product_thumbnail`: the product's lead image, shown in lists;
192
220
  * - `variant` / `variant_thumbnail`: a variant's own image and its lead image
@@ -208,7 +236,7 @@ declare const UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix>;
208
236
  * on the merchant's products. No parameters. The API reference says
209
237
  * "internal SKU tags are excluded", but the live API includes system tags
210
238
  * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;
211
- * test merchant 5644, 29 Oct 2026). Apps that offer the list as suggestions
239
+ * a test merchant, 29 Oct 2026). Apps that offer the list as suggestions
212
240
  * filter those themselves. Tags of deleted products drop out of the list.
213
241
  * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,
214
242
  * the same for price instances (channels such as "Eat In", "Takeout").
@@ -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 {
@@ -48,7 +48,7 @@ function tokenFromResponse(response, previous, now = Date.now()) {
48
48
  }
49
49
  /**
50
50
  * Reads a token from session storage. Accepts both the current shape and the
51
- * legacy raw response that older apps (e.g. AdminUI ≤ 0.1) stored directly in
51
+ * legacy raw response that older apps stored directly in
52
52
  * the session, so existing sessions survive an upgrade.
53
53
  */
54
54
  function normalizeStoredToken(raw) {
@@ -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
 
@@ -478,7 +509,7 @@ function isPlainObject(value) {
478
509
  *
479
510
  * Accepts:
480
511
  * - the SDK ≥ 0.3 shape `{ <collection>: { "0": {...}, "1": {...} } }` (refs kept),
481
- * - the older AdminUI shapes `{ <collection>: { new: {...} } }` / `{ updated: {...} }`,
512
+ * - the older app shapes `{ <collection>: { new: {...} } }` / `{ updated: {...} }`,
482
513
  * - a bare entity object `{ title: … }`.
483
514
  *
484
515
  * With `forcedId` (item routes) exactly one entity is allowed and its `id` is
@@ -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);