react-shopwave-connect 0.4.2 → 0.4.3

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.
package/README.md CHANGED
@@ -226,7 +226,7 @@ The shapes below are the ones adminV1 saves and adminV2 reads, so all three admi
226
226
  | category | `fetchCategories`, `fetchCategory`, `saveCategory`, `deleteCategory` |
227
227
  | consumer | `fetchConsumers`, `fetchConsumer` (read-only) |
228
228
  | employee | `fetchEmployees`, `fetchEmployee`, `saveEmployee`, `deleteEmployee` |
229
- | product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice` |
229
+ | product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice`; tag lists `fetchProductTags`, `fetchProductInstanceTags` (`string[]`), `normalizeTagList` |
230
230
  | promotion | `fetchPromotions`, `fetchPromotion`, `savePromotion`, `deletePromotion` |
231
231
  | store | `fetchStores` (now with `storeIds`), `fetchStore`, `saveStore`, `deleteStore` |
232
232
  | report | `fetchReport` |
@@ -281,7 +281,7 @@ const { data } = useProduct({ storeId }, { baseUrl: "https://api.merchantstack.c
281
281
 
282
282
  ### Auto-fetch hooks (`useEffect`-based)
283
283
 
284
- `useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession`, `useMerchant`, `useUser` (`useUser({ enabled: false })` stays idle).
284
+ `useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `useProductTags`, `useProductInstanceTags`, `usePromotion`, `useStore`, `useReport`, `useSession`, `useMerchant`, `useUser` (`useUser({ enabled: false })` stays idle; so do the tag hooks, so a tag field can load its list the first time it opens).
285
285
  They fetch on mount and re-run when their arguments change. `useConsumer` and `useReport` stay idle until you pass ids / a query (they return `loading: false`, `data: null` until then). `refetch()` replaces the old `reloadFlag` argument.
286
286
 
287
287
  ### Manually-triggered hooks (`useCallback`-based)
@@ -467,8 +467,15 @@ export const { GET } = shopwave.resource("user");
467
467
 
468
468
  // app/api/upload/route.ts — image upload (uploadImage)
469
469
  export const { POST } = shopwave.upload(); // or upload({ maxBytes, accept })
470
+
471
+ // app/api/products/tags/route.ts — the merchant's product tags (fetchProductTags)
472
+ export const { GET } = shopwave.lookup("productTags");
473
+ // app/api/products/instance-tags/route.ts — price-instance (channel) tags (fetchProductInstanceTags)
474
+ export const { GET } = shopwave.lookup("productInstanceTags");
470
475
  ```
471
476
 
477
+ - `lookup(kind)`: the read-only lists in `SHOPWAVE_LOOKUPS`. `productTags` → `GET /product/tags`, `productInstanceTags` → `GET /product/instance/tags`; both answer `{ tags: string[] }`. They take no parameters, so `extras` aren't forwarded. The app routes are `products/tags` and `products/instance-tags`: static segments, so they sit beside `products/[id]`.
478
+
472
479
  - `resource("merchant")`: `PUT`/`POST` take `{ merchant: {…} }` or the bare object and send every field on as `postBody`. The `id` is required (400 without it), because a POST without one would create a second merchant. `resource("user")` answers 405 to writes.
473
480
  - `upload()`: takes multipart `file` + `kind`, checks the size (default 10 MB → 413) and type (default `image/*` → 415), and `PUT`s only the file to `/uploader` with the `contentType` header. `extras` are ignored on uploads. Shopwave's reply (201 `{ fileName, path }`) is passed through.
474
481
 
@@ -503,6 +510,11 @@ Options: `apiVersion` (default `"2.0"`), `fetch`, `entities` (override a route/h
503
510
 
504
511
  ---
505
512
 
513
+ ## Contributing
514
+
515
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): how the package is built and the rules for changing it: layers, request flow, contract tables, endpoint kinds (and how to choose one), a checklist for adding an endpoint, code rules, known gaps, and versioning.
516
+ - [CLAUDE.md](CLAUDE.md): the short rules for coding agents working in this repo.
517
+
506
518
  ## Build
507
519
 
508
520
  Built with [`tsdown`](https://tsdown.dev) into dual ESM + CJS with `.d.ts` types, via separate entries: `core` (no externals), `hooks` (`react`/`react-dom` external), `server` (no externals) and `next` (`next`/`iron-session` external — optional peer dependencies). `server` and `next` are never re-exported from the root entry, so they can't end up in client bundles.
@@ -350,6 +350,106 @@ async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
350
350
  return result;
351
351
  }
352
352
 
353
+ //#endregion
354
+ //#region src/core/resources.ts
355
+ /**
356
+ * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
357
+ * updates it (without an `id` the API would create a second merchant, so the
358
+ * route requires one).
359
+ * - user: `GET /user` (read-only here; the API's `POST /user` invites users and
360
+ * needs the admin scope).
361
+ */
362
+ const SHOPWAVE_RESOURCES = Object.freeze({
363
+ merchant: {
364
+ route: "merchant",
365
+ key: "merchant",
366
+ upstream: "merchant",
367
+ writable: true
368
+ },
369
+ user: {
370
+ route: "user",
371
+ key: "user",
372
+ upstream: "user",
373
+ writable: false
374
+ }
375
+ });
376
+ /** `UploadKind` → the API's `contentType` value. */
377
+ const UPLOAD_KINDS = Object.freeze({
378
+ merchant: "merchant",
379
+ merchantFeature: "merchantFeature",
380
+ product: "product",
381
+ user: "user",
382
+ applicationLogo: "applicaionLogo",
383
+ applicationImages: "applicaionImages"
384
+ });
385
+ /** How uploads are addressed (app route and Shopwave uploader). */
386
+ const SHOPWAVE_UPLOAD = Object.freeze({
387
+ route: "upload",
388
+ upstream: "uploader",
389
+ kindHeader: "contentType",
390
+ fileField: "file",
391
+ kindField: "kind"
392
+ });
393
+ function isUploadKind(value) {
394
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
395
+ }
396
+ const SHOPWAVE_LOOKUPS = Object.freeze({
397
+ productTags: {
398
+ route: "products/tags",
399
+ key: "tags",
400
+ upstream: "product/tags"
401
+ },
402
+ productInstanceTags: {
403
+ route: "products/instance-tags",
404
+ key: "tags",
405
+ upstream: "product/instance/tags"
406
+ }
407
+ });
408
+
409
+ //#endregion
410
+ //#region src/core/productTags.ts
411
+ /**
412
+ * Turns a tag list from the API into clean strings: trims each tag, drops
413
+ * blanks and anything that isn't text, and removes exact duplicates (first one
414
+ * wins). The API's own order is kept. A map is accepted too (its values are
415
+ * used), in case the API ever answers keyed by id.
416
+ */
417
+ function normalizeTagList(value) {
418
+ const list = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
419
+ const seen = /* @__PURE__ */ new Set();
420
+ const out = [];
421
+ for (const item of list) {
422
+ if (typeof item !== "string" && typeof item !== "number") continue;
423
+ const tag = String(item).trim();
424
+ if (!tag || seen.has(tag)) continue;
425
+ seen.add(tag);
426
+ out.push(tag);
427
+ }
428
+ return out;
429
+ }
430
+ async function fetchLookup(kind, options) {
431
+ const def = SHOPWAVE_LOOKUPS[kind];
432
+ if (!def) throw new Error(`Unknown Shopwave lookup "${kind}"`);
433
+ const json = await apiGet(`/api/${def.route}`, {}, options);
434
+ assertNoApiErrors(json, 200);
435
+ return normalizeTagList(json?.[def.key]);
436
+ }
437
+ /**
438
+ * Every distinct tag used on the merchant's products (`GET /product/tags`),
439
+ * for a tag field's suggestions. Returns `[]` when there are none.
440
+ */
441
+ function fetchProductTags(options = {}) {
442
+ return fetchLookup("productTags", options);
443
+ }
444
+ /**
445
+ * Every distinct tag used on the merchant's price instances
446
+ * (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
447
+ * Returns `[]` when there are none.
448
+ */
449
+ function fetchProductInstanceTags(options = {}) {
450
+ return fetchLookup("productInstanceTags", options);
451
+ }
452
+
353
453
  //#endregion
354
454
  //#region src/core/promotion.ts
355
455
  /** Promotion rule identifiers as stored by the Shopwave API. */
@@ -811,50 +911,6 @@ function buildConsumerMap(consumers) {
811
911
  return map;
812
912
  }
813
913
 
814
- //#endregion
815
- //#region src/core/resources.ts
816
- /**
817
- * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
818
- * updates it (without an `id` the API would create a second merchant, so the
819
- * route requires one).
820
- * - user: `GET /user` (read-only here; the API's `POST /user` invites users and
821
- * needs the admin scope).
822
- */
823
- const SHOPWAVE_RESOURCES = Object.freeze({
824
- merchant: {
825
- route: "merchant",
826
- key: "merchant",
827
- upstream: "merchant",
828
- writable: true
829
- },
830
- user: {
831
- route: "user",
832
- key: "user",
833
- upstream: "user",
834
- writable: false
835
- }
836
- });
837
- /** `UploadKind` → the API's `contentType` value. */
838
- const UPLOAD_KINDS = Object.freeze({
839
- merchant: "merchant",
840
- merchantFeature: "merchantFeature",
841
- product: "product",
842
- user: "user",
843
- applicationLogo: "applicaionLogo",
844
- applicationImages: "applicaionImages"
845
- });
846
- /** How uploads are addressed (app route and Shopwave uploader). */
847
- const SHOPWAVE_UPLOAD = Object.freeze({
848
- route: "upload",
849
- upstream: "uploader",
850
- kindHeader: "contentType",
851
- fileField: "file",
852
- kindField: "kind"
853
- });
854
- function isUploadKind(value) {
855
- return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
856
- }
857
-
858
914
  //#endregion
859
915
  //#region src/core/imageFit.ts
860
916
  /**
@@ -1305,6 +1361,7 @@ exports.MERCHANT_READ_ONLY_FIELDS = MERCHANT_READ_ONLY_FIELDS;
1305
1361
  exports.PromotionRuleSet = PromotionRuleSet;
1306
1362
  exports.SHOPWAVE_ENTITIES = SHOPWAVE_ENTITIES;
1307
1363
  exports.SHOPWAVE_IMAGE_BASE_URL = SHOPWAVE_IMAGE_BASE_URL;
1364
+ exports.SHOPWAVE_LOOKUPS = SHOPWAVE_LOOKUPS;
1308
1365
  exports.SHOPWAVE_RESOURCES = SHOPWAVE_RESOURCES;
1309
1366
  exports.SHOPWAVE_UPLOAD = SHOPWAVE_UPLOAD;
1310
1367
  exports.ShopwaveApiError = ShopwaveApiError;
@@ -1338,6 +1395,8 @@ exports.fetchEmployees = fetchEmployees;
1338
1395
  exports.fetchEntityById = fetchEntityById;
1339
1396
  exports.fetchMerchant = fetchMerchant;
1340
1397
  exports.fetchProduct = fetchProduct;
1398
+ exports.fetchProductInstanceTags = fetchProductInstanceTags;
1399
+ exports.fetchProductTags = fetchProductTags;
1341
1400
  exports.fetchProducts = fetchProducts;
1342
1401
  exports.fetchProductsMap = fetchProductsMap;
1343
1402
  exports.fetchPromotion = fetchPromotion;
@@ -1364,6 +1423,7 @@ exports.logoutPath = logoutPath;
1364
1423
  exports.merchantLinksFromForm = merchantLinksFromForm;
1365
1424
  exports.merchantLinksToForm = merchantLinksToForm;
1366
1425
  exports.normalizeMerchantLinks = normalizeMerchantLinks;
1426
+ exports.normalizeTagList = normalizeTagList;
1367
1427
  exports.parseBasketReportData = parseBasketReportData;
1368
1428
  exports.readImageSize = readImageSize;
1369
1429
  exports.rolesById = rolesById;