react-shopwave-connect 0.3.0 → 0.4.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.
@@ -345,6 +345,50 @@ const SHOPWAVE_ENTITIES = Object.freeze({
345
345
  }
346
346
  });
347
347
 
348
+ //#endregion
349
+ //#region src/core/resources.ts
350
+ /**
351
+ * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
352
+ * updates it (without an `id` the API would create a second merchant, so the
353
+ * route requires one).
354
+ * - user: `GET /user` (read-only here; the API's `POST /user` invites users and
355
+ * needs the admin scope).
356
+ */
357
+ const SHOPWAVE_RESOURCES = Object.freeze({
358
+ merchant: {
359
+ route: "merchant",
360
+ key: "merchant",
361
+ upstream: "merchant",
362
+ writable: true
363
+ },
364
+ user: {
365
+ route: "user",
366
+ key: "user",
367
+ upstream: "user",
368
+ writable: false
369
+ }
370
+ });
371
+ /** `UploadKind` → the API's `contentType` value. */
372
+ const UPLOAD_KINDS = Object.freeze({
373
+ merchant: "merchant",
374
+ merchantFeature: "merchantFeature",
375
+ product: "product",
376
+ user: "user",
377
+ applicationLogo: "applicaionLogo",
378
+ applicationImages: "applicaionImages"
379
+ });
380
+ /** How uploads are addressed (app route and Shopwave uploader). */
381
+ const SHOPWAVE_UPLOAD = Object.freeze({
382
+ route: "upload",
383
+ upstream: "uploader",
384
+ kindHeader: "contentType",
385
+ fileField: "file",
386
+ kindField: "kind"
387
+ });
388
+ function isUploadKind(value) {
389
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
390
+ }
391
+
348
392
  //#endregion
349
393
  //#region src/server/api.ts
350
394
  /**
@@ -543,13 +587,14 @@ function createShopwaveApiHandlers(config) {
543
587
  return jsonError(502, "auth_unavailable", "Could not refresh the Shopwave session");
544
588
  }
545
589
  if (!authorization) return jsonError(401, "unauthorized", "Not logged in");
546
- const body = init.postBody === void 0 ? void 0 : new URLSearchParams({ postBody: JSON.stringify(init.postBody) });
590
+ const body = init.postBody !== void 0 ? new URLSearchParams({ postBody: JSON.stringify(init.postBody) }) : init.formData;
547
591
  const call = (authz) => {
548
592
  const headers = new Headers(extras);
549
593
  for (const [k, v] of Object.entries(init.headers ?? {})) headers.set(k, v);
550
594
  headers.set("Authorization", authz);
551
595
  headers.set("x-accept-version", apiVersion);
552
- if (body) headers.set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
596
+ if (body instanceof URLSearchParams) headers.set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
597
+ else if (body) headers.delete("Content-Type");
553
598
  return (config.fetch ?? fetch)(`${apiBase()}/${path}`, {
554
599
  method,
555
600
  headers,
@@ -612,6 +657,66 @@ function createShopwaveApiHandlers(config) {
612
657
  postBody
613
658
  });
614
659
  }
660
+ function resourceDef(kind) {
661
+ const def = SHOPWAVE_RESOURCES[kind];
662
+ if (!def) throw new Error(`Unknown Shopwave resource "${kind}"`);
663
+ return def;
664
+ }
665
+ async function writeResource(request, def) {
666
+ if (!def.writable) return jsonError(405, "method_not_allowed", `The Shopwave API doesn't support writes for ${def.key}`);
667
+ let record;
668
+ try {
669
+ const body = await readJsonBody(request);
670
+ if (!isPlainObject(body)) throw new RequestBodyError("Request body must be a JSON object");
671
+ const inner = body[def.key];
672
+ if (inner !== void 0 && !isPlainObject(inner)) throw new RequestBodyError(`"${def.key}" must be an object`);
673
+ record = { ...inner ?? body };
674
+ const id = record.id;
675
+ if (id == null || id === "" || !ID_PATTERN.test(String(id))) throw new RequestBodyError(`Missing or invalid ${def.key} id`);
676
+ record.id = /^\d+$/.test(String(id)) ? Number(id) : id;
677
+ } catch (error) {
678
+ if (error instanceof RequestBodyError) return jsonError(400, "invalid_body", error.message);
679
+ throw error;
680
+ }
681
+ return forward(request, {
682
+ method: "POST",
683
+ path: def.upstream,
684
+ postBody: { [def.key]: record }
685
+ });
686
+ }
687
+ function acceptsType(type, accept) {
688
+ const t = type.toLowerCase().split(";")[0].trim();
689
+ return accept.some((a) => {
690
+ const rule = a.toLowerCase();
691
+ return rule.endsWith("/") ? t.startsWith(rule) : t === rule;
692
+ });
693
+ }
694
+ async function uploadFile(request, options) {
695
+ const maxBytes = options.maxBytes ?? 10 * 1024 * 1024;
696
+ const accept = options.accept ?? ["image/"];
697
+ if (Number(request.headers.get("content-length") ?? 0) > maxBytes + 64 * 1024) return jsonError(413, "too_large", `The file is larger than ${maxBytes} bytes`);
698
+ let form;
699
+ try {
700
+ form = await request.formData();
701
+ } catch {
702
+ return jsonError(400, "invalid_body", "Expected a multipart/form-data body");
703
+ }
704
+ const file = form.get(SHOPWAVE_UPLOAD.fileField);
705
+ if (!file || typeof file === "string" || file.size === 0) return jsonError(400, "invalid_body", `Missing "${SHOPWAVE_UPLOAD.fileField}"`);
706
+ const kind = form.get(SHOPWAVE_UPLOAD.kindField);
707
+ if (!isUploadKind(kind)) return jsonError(400, "invalid_body", `"${SHOPWAVE_UPLOAD.kindField}" must be one of ${Object.keys(UPLOAD_KINDS).join(", ")}`);
708
+ if (file.size > maxBytes) return jsonError(413, "too_large", `The file is larger than ${maxBytes} bytes`);
709
+ if (!acceptsType(file.type || "", accept)) return jsonError(415, "unsupported_type", `Files of type "${file.type || "unknown"}" aren't accepted`);
710
+ const upstreamForm = new FormData();
711
+ upstreamForm.append(SHOPWAVE_UPLOAD.fileField, file, file.name || "upload");
712
+ return forward(request, {
713
+ method: "PUT",
714
+ path: SHOPWAVE_UPLOAD.upstream,
715
+ headers: { [SHOPWAVE_UPLOAD.kindHeader]: UPLOAD_KINDS[kind] },
716
+ formData: upstreamForm,
717
+ forwardExtras: false
718
+ });
719
+ }
615
720
  function badId() {
616
721
  return jsonError(400, "invalid_id", "Missing or invalid id");
617
722
  }
@@ -671,6 +776,18 @@ function createShopwaveApiHandlers(config) {
671
776
  },
672
777
  passthrough(path) {
673
778
  return { GET: (request) => forward(request, { path }) };
779
+ },
780
+ resource(kind) {
781
+ const def = resourceDef(kind);
782
+ const save = (request) => writeResource(request, def);
783
+ return {
784
+ GET: (request) => forward(request, { path: def.upstream }),
785
+ PUT: save,
786
+ POST: save
787
+ };
788
+ },
789
+ upload(options = {}) {
790
+ return { POST: (request) => uploadFile(request, options) };
674
791
  }
675
792
  };
676
793
  }
@@ -997,6 +1114,11 @@ function createShopwaveAuth(config) {
997
1114
  * export const { GET, PUT, DELETE } = shopwave.item("product");
998
1115
  * // app/api/report/route.ts
999
1116
  * export const { GET } = shopwave.passthrough("report");
1117
+ * // app/api/merchant/route.ts, app/api/user/route.ts
1118
+ * export const { GET, PUT } = shopwave.resource("merchant");
1119
+ * export const { GET } = shopwave.resource("user");
1120
+ * // app/api/upload/route.ts
1121
+ * export const { POST } = shopwave.upload();
1000
1122
  * ```
1001
1123
  */
1002
1124
  function createShopwaveApi(config) {
@@ -1010,7 +1132,9 @@ function createShopwaveApi(config) {
1010
1132
 
1011
1133
  //#endregion
1012
1134
  exports.SHOPWAVE_ENTITIES = SHOPWAVE_ENTITIES;
1135
+ exports.SHOPWAVE_RESOURCES = SHOPWAVE_RESOURCES;
1013
1136
  exports.ShopwaveAuthError = ShopwaveAuthError;
1137
+ exports.UPLOAD_KINDS = UPLOAD_KINDS;
1014
1138
  exports.authorizationHeader = authorizationHeader;
1015
1139
  exports.createShopwaveApi = createShopwaveApi;
1016
1140
  exports.createShopwaveAuth = createShopwaveAuth;