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.
- package/README.md +71 -3
- package/dist/core/index.cjs +511 -0
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +391 -1
- package/dist/core/index.d.ts +391 -1
- package/dist/core/index.js +481 -1
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +456 -0
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +229 -1
- package/dist/hooks/index.d.ts +229 -1
- package/dist/hooks/index.js +452 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +595 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +444 -1
- package/dist/index.d.ts +444 -1
- package/dist/index.js +560 -1
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +126 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +82 -2
- package/dist/next/index.d.ts +82 -2
- package/dist/next/index.js +125 -3
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +122 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +90 -2
- package/dist/server/index.d.ts +90 -2
- package/dist/server/index.js +120 -3
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/next/index.d.ts
CHANGED
|
@@ -127,6 +127,46 @@ type EntityKind = "product" | "category" | "store" | "promotion" | "employee" |
|
|
|
127
127
|
*/
|
|
128
128
|
declare const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>;
|
|
129
129
|
//#endregion
|
|
130
|
+
//#region src/core/resources.d.ts
|
|
131
|
+
/**
|
|
132
|
+
* Singleton resources (one per logged-in user) and the uploader.
|
|
133
|
+
*
|
|
134
|
+
* Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions
|
|
135
|
+
* (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route
|
|
136
|
+
* handlers in `react-shopwave-connect/next`, so the two halves of the contract
|
|
137
|
+
* can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't
|
|
138
|
+
* keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.
|
|
139
|
+
*/
|
|
140
|
+
interface ResourceDefinition {
|
|
141
|
+
/** App route: `/api/<route>`. */
|
|
142
|
+
route: string;
|
|
143
|
+
/** Key of the object in requests and responses (`{ merchant: {…} }`). */
|
|
144
|
+
key: string;
|
|
145
|
+
/** Shopwave API path. */
|
|
146
|
+
upstream: string;
|
|
147
|
+
/** False for read-only resources. */
|
|
148
|
+
writable: boolean;
|
|
149
|
+
}
|
|
150
|
+
type ResourceKind = "merchant" | "user";
|
|
151
|
+
/**
|
|
152
|
+
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
153
|
+
* updates it (without an `id` the API would create a second merchant, so the
|
|
154
|
+
* route requires one).
|
|
155
|
+
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
156
|
+
* needs the admin scope).
|
|
157
|
+
*/
|
|
158
|
+
declare const SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>>;
|
|
159
|
+
/**
|
|
160
|
+
* What an upload is for. Sent to Shopwave as the `contentType` header of
|
|
161
|
+
* `PUT /uploader`. `merchant` is for the receipt and square logos (stored
|
|
162
|
+
* under `…/merchant/<id>/logo/`), `merchantFeature` for the featured image.
|
|
163
|
+
* The API spells two of its values `applicaionLogo` / `applicaionImages`; use
|
|
164
|
+
* the correctly spelt names here and the server maps them.
|
|
165
|
+
*/
|
|
166
|
+
type UploadKind = "merchant" | "merchantFeature" | "product" | "user" | "applicationLogo" | "applicationImages";
|
|
167
|
+
/** `UploadKind` → the API's `contentType` value. */
|
|
168
|
+
declare const UPLOAD_KINDS: Readonly<Record<UploadKind, string>>;
|
|
169
|
+
//#endregion
|
|
130
170
|
//#region src/server/api.d.ts
|
|
131
171
|
/**
|
|
132
172
|
* The OAuth token a caller sent with the request, without its scheme, or `null`.
|
|
@@ -180,6 +220,8 @@ interface ForwardInit {
|
|
|
180
220
|
headers?: Record<string, string>;
|
|
181
221
|
/** Sent as the form field `postBody=<JSON>`. */
|
|
182
222
|
postBody?: unknown;
|
|
223
|
+
/** Sent as a multipart body (e.g. uploads). Ignored when `postBody` is set. */
|
|
224
|
+
formData?: FormData;
|
|
183
225
|
/** Forward the caller's `extras` header as upstream headers. Defaults to `true`. */
|
|
184
226
|
forwardExtras?: boolean;
|
|
185
227
|
}
|
|
@@ -204,6 +246,35 @@ interface ItemHandlers {
|
|
|
204
246
|
/** Delete the record at `[id]`. Shopwave answers 205 with an empty body. */
|
|
205
247
|
DELETE: RouteHandler;
|
|
206
248
|
}
|
|
249
|
+
interface ResourceHandlers {
|
|
250
|
+
/** Read the resource (`GET {apiUrl}/<upstream>`). */
|
|
251
|
+
GET: RouteHandler;
|
|
252
|
+
/**
|
|
253
|
+
* Update it: body `{ <key>: { id, … } }` or the bare object. The `id` is
|
|
254
|
+
* required (without one Shopwave would create a new record). 405 for
|
|
255
|
+
* read-only resources.
|
|
256
|
+
*/
|
|
257
|
+
PUT: RouteHandler;
|
|
258
|
+
/** Same as PUT. */
|
|
259
|
+
POST: RouteHandler;
|
|
260
|
+
}
|
|
261
|
+
interface UploadOptions {
|
|
262
|
+
/** Largest accepted file, in bytes. Defaults to 10 MB. */
|
|
263
|
+
maxBytes?: number;
|
|
264
|
+
/**
|
|
265
|
+
* Accepted MIME types. An entry ending in `/` is a prefix (`"image/"`).
|
|
266
|
+
* Defaults to `["image/"]`.
|
|
267
|
+
*/
|
|
268
|
+
accept?: string[];
|
|
269
|
+
}
|
|
270
|
+
interface UploadHandlers {
|
|
271
|
+
/**
|
|
272
|
+
* Multipart `file` + `kind` (see `UploadKind`) → `PUT {apiUrl}/uploader`.
|
|
273
|
+
* Answers with Shopwave's reply (201 `{ fileName, path, api }`); 400 for a
|
|
274
|
+
* missing file or unknown kind, 413 when too large, 415 for other types.
|
|
275
|
+
*/
|
|
276
|
+
POST: RouteHandler;
|
|
277
|
+
}
|
|
207
278
|
interface ShopwaveApiHandlers {
|
|
208
279
|
/** Calls the Shopwave API for this request (token, extras, refresh-retry) and returns its answer. */
|
|
209
280
|
forward(request: Request, init: ForwardInit): Promise<Response>;
|
|
@@ -213,10 +284,14 @@ interface ShopwaveApiHandlers {
|
|
|
213
284
|
item(kind: EntityKind, options?: {
|
|
214
285
|
param?: string;
|
|
215
286
|
}): ItemHandlers;
|
|
216
|
-
/** GET-only proxy for a Shopwave path (e.g. `"report"
|
|
287
|
+
/** GET-only proxy for a Shopwave path (e.g. `"report"`). */
|
|
217
288
|
passthrough(path: string): {
|
|
218
289
|
GET: RouteHandler;
|
|
219
290
|
};
|
|
291
|
+
/** Handlers for a singleton resource: `app/api/merchant/route.ts`, `app/api/user/route.ts`. */
|
|
292
|
+
resource(kind: ResourceKind): ResourceHandlers;
|
|
293
|
+
/** Handler for `app/api/upload/route.ts`. */
|
|
294
|
+
upload(options?: UploadOptions): UploadHandlers;
|
|
220
295
|
/** The entity table in use (defaults merged with `config.entities`). */
|
|
221
296
|
entity(kind: EntityKind): EntityDefinition;
|
|
222
297
|
}
|
|
@@ -415,9 +490,14 @@ interface ShopwaveNextApiConfig extends Omit<ShopwaveApiConfig, "getAuthorizatio
|
|
|
415
490
|
* export const { GET, PUT, DELETE } = shopwave.item("product");
|
|
416
491
|
* // app/api/report/route.ts
|
|
417
492
|
* export const { GET } = shopwave.passthrough("report");
|
|
493
|
+
* // app/api/merchant/route.ts, app/api/user/route.ts
|
|
494
|
+
* export const { GET, PUT } = shopwave.resource("merchant");
|
|
495
|
+
* export const { GET } = shopwave.resource("user");
|
|
496
|
+
* // app/api/upload/route.ts
|
|
497
|
+
* export const { POST } = shopwave.upload();
|
|
418
498
|
* ```
|
|
419
499
|
*/
|
|
420
500
|
declare function createShopwaveApi(config: ShopwaveNextApiConfig): ShopwaveApiHandlers;
|
|
421
501
|
//#endregion
|
|
422
|
-
export { AuthContext, type CollectionHandlers, type EntityDefinition, type EntityKind, type ForwardInit, type ItemHandlers, ProtectOptions, type RouteContext, type RouteHandler, SHOPWAVE_ENTITIES, type SessionStatus, type ShopwaveApiHandlers, ShopwaveAuth, ShopwaveAuthConfig, ShopwaveAuthError, ShopwaveNextApiConfig, ShopwaveSessionConfig, ShopwaveSessionData, type ShopwaveToken, authorizationHeader, createShopwaveApi, createShopwaveAuth, isExpiredTokenResponse, readRequestToken };
|
|
502
|
+
export { AuthContext, type CollectionHandlers, type EntityDefinition, type EntityKind, type ForwardInit, type ItemHandlers, ProtectOptions, type ResourceHandlers, type ResourceKind, type RouteContext, type RouteHandler, SHOPWAVE_ENTITIES, SHOPWAVE_RESOURCES, type SessionStatus, type ShopwaveApiHandlers, ShopwaveAuth, ShopwaveAuthConfig, ShopwaveAuthError, ShopwaveNextApiConfig, ShopwaveSessionConfig, ShopwaveSessionData, type ShopwaveToken, UPLOAD_KINDS, type UploadHandlers, type UploadKind, type UploadOptions, authorizationHeader, createShopwaveApi, createShopwaveAuth, isExpiredTokenResponse, readRequestToken };
|
|
423
503
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/next/index.js
CHANGED
|
@@ -319,6 +319,50 @@ const SHOPWAVE_ENTITIES = Object.freeze({
|
|
|
319
319
|
}
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/core/resources.ts
|
|
324
|
+
/**
|
|
325
|
+
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
326
|
+
* updates it (without an `id` the API would create a second merchant, so the
|
|
327
|
+
* route requires one).
|
|
328
|
+
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
329
|
+
* needs the admin scope).
|
|
330
|
+
*/
|
|
331
|
+
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
332
|
+
merchant: {
|
|
333
|
+
route: "merchant",
|
|
334
|
+
key: "merchant",
|
|
335
|
+
upstream: "merchant",
|
|
336
|
+
writable: true
|
|
337
|
+
},
|
|
338
|
+
user: {
|
|
339
|
+
route: "user",
|
|
340
|
+
key: "user",
|
|
341
|
+
upstream: "user",
|
|
342
|
+
writable: false
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
/** `UploadKind` → the API's `contentType` value. */
|
|
346
|
+
const UPLOAD_KINDS = Object.freeze({
|
|
347
|
+
merchant: "merchant",
|
|
348
|
+
merchantFeature: "merchantFeature",
|
|
349
|
+
product: "product",
|
|
350
|
+
user: "user",
|
|
351
|
+
applicationLogo: "applicaionLogo",
|
|
352
|
+
applicationImages: "applicaionImages"
|
|
353
|
+
});
|
|
354
|
+
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
355
|
+
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
356
|
+
route: "upload",
|
|
357
|
+
upstream: "uploader",
|
|
358
|
+
kindHeader: "contentType",
|
|
359
|
+
fileField: "file",
|
|
360
|
+
kindField: "kind"
|
|
361
|
+
});
|
|
362
|
+
function isUploadKind(value) {
|
|
363
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
364
|
+
}
|
|
365
|
+
|
|
322
366
|
//#endregion
|
|
323
367
|
//#region src/server/api.ts
|
|
324
368
|
/**
|
|
@@ -517,13 +561,14 @@ function createShopwaveApiHandlers(config) {
|
|
|
517
561
|
return jsonError(502, "auth_unavailable", "Could not refresh the Shopwave session");
|
|
518
562
|
}
|
|
519
563
|
if (!authorization) return jsonError(401, "unauthorized", "Not logged in");
|
|
520
|
-
const body = init.postBody
|
|
564
|
+
const body = init.postBody !== void 0 ? new URLSearchParams({ postBody: JSON.stringify(init.postBody) }) : init.formData;
|
|
521
565
|
const call = (authz) => {
|
|
522
566
|
const headers = new Headers(extras);
|
|
523
567
|
for (const [k, v] of Object.entries(init.headers ?? {})) headers.set(k, v);
|
|
524
568
|
headers.set("Authorization", authz);
|
|
525
569
|
headers.set("x-accept-version", apiVersion);
|
|
526
|
-
if (body) headers.set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
570
|
+
if (body instanceof URLSearchParams) headers.set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
571
|
+
else if (body) headers.delete("Content-Type");
|
|
527
572
|
return (config.fetch ?? fetch)(`${apiBase()}/${path}`, {
|
|
528
573
|
method,
|
|
529
574
|
headers,
|
|
@@ -586,6 +631,66 @@ function createShopwaveApiHandlers(config) {
|
|
|
586
631
|
postBody
|
|
587
632
|
});
|
|
588
633
|
}
|
|
634
|
+
function resourceDef(kind) {
|
|
635
|
+
const def = SHOPWAVE_RESOURCES[kind];
|
|
636
|
+
if (!def) throw new Error(`Unknown Shopwave resource "${kind}"`);
|
|
637
|
+
return def;
|
|
638
|
+
}
|
|
639
|
+
async function writeResource(request, def) {
|
|
640
|
+
if (!def.writable) return jsonError(405, "method_not_allowed", `The Shopwave API doesn't support writes for ${def.key}`);
|
|
641
|
+
let record;
|
|
642
|
+
try {
|
|
643
|
+
const body = await readJsonBody(request);
|
|
644
|
+
if (!isPlainObject(body)) throw new RequestBodyError("Request body must be a JSON object");
|
|
645
|
+
const inner = body[def.key];
|
|
646
|
+
if (inner !== void 0 && !isPlainObject(inner)) throw new RequestBodyError(`"${def.key}" must be an object`);
|
|
647
|
+
record = { ...inner ?? body };
|
|
648
|
+
const id = record.id;
|
|
649
|
+
if (id == null || id === "" || !ID_PATTERN.test(String(id))) throw new RequestBodyError(`Missing or invalid ${def.key} id`);
|
|
650
|
+
record.id = /^\d+$/.test(String(id)) ? Number(id) : id;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
if (error instanceof RequestBodyError) return jsonError(400, "invalid_body", error.message);
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
return forward(request, {
|
|
656
|
+
method: "POST",
|
|
657
|
+
path: def.upstream,
|
|
658
|
+
postBody: { [def.key]: record }
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
function acceptsType(type, accept) {
|
|
662
|
+
const t = type.toLowerCase().split(";")[0].trim();
|
|
663
|
+
return accept.some((a) => {
|
|
664
|
+
const rule = a.toLowerCase();
|
|
665
|
+
return rule.endsWith("/") ? t.startsWith(rule) : t === rule;
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
async function uploadFile(request, options) {
|
|
669
|
+
const maxBytes = options.maxBytes ?? 10 * 1024 * 1024;
|
|
670
|
+
const accept = options.accept ?? ["image/"];
|
|
671
|
+
if (Number(request.headers.get("content-length") ?? 0) > maxBytes + 64 * 1024) return jsonError(413, "too_large", `The file is larger than ${maxBytes} bytes`);
|
|
672
|
+
let form;
|
|
673
|
+
try {
|
|
674
|
+
form = await request.formData();
|
|
675
|
+
} catch {
|
|
676
|
+
return jsonError(400, "invalid_body", "Expected a multipart/form-data body");
|
|
677
|
+
}
|
|
678
|
+
const file = form.get(SHOPWAVE_UPLOAD.fileField);
|
|
679
|
+
if (!file || typeof file === "string" || file.size === 0) return jsonError(400, "invalid_body", `Missing "${SHOPWAVE_UPLOAD.fileField}"`);
|
|
680
|
+
const kind = form.get(SHOPWAVE_UPLOAD.kindField);
|
|
681
|
+
if (!isUploadKind(kind)) return jsonError(400, "invalid_body", `"${SHOPWAVE_UPLOAD.kindField}" must be one of ${Object.keys(UPLOAD_KINDS).join(", ")}`);
|
|
682
|
+
if (file.size > maxBytes) return jsonError(413, "too_large", `The file is larger than ${maxBytes} bytes`);
|
|
683
|
+
if (!acceptsType(file.type || "", accept)) return jsonError(415, "unsupported_type", `Files of type "${file.type || "unknown"}" aren't accepted`);
|
|
684
|
+
const upstreamForm = new FormData();
|
|
685
|
+
upstreamForm.append(SHOPWAVE_UPLOAD.fileField, file, file.name || "upload");
|
|
686
|
+
return forward(request, {
|
|
687
|
+
method: "PUT",
|
|
688
|
+
path: SHOPWAVE_UPLOAD.upstream,
|
|
689
|
+
headers: { [SHOPWAVE_UPLOAD.kindHeader]: UPLOAD_KINDS[kind] },
|
|
690
|
+
formData: upstreamForm,
|
|
691
|
+
forwardExtras: false
|
|
692
|
+
});
|
|
693
|
+
}
|
|
589
694
|
function badId() {
|
|
590
695
|
return jsonError(400, "invalid_id", "Missing or invalid id");
|
|
591
696
|
}
|
|
@@ -645,6 +750,18 @@ function createShopwaveApiHandlers(config) {
|
|
|
645
750
|
},
|
|
646
751
|
passthrough(path) {
|
|
647
752
|
return { GET: (request) => forward(request, { path }) };
|
|
753
|
+
},
|
|
754
|
+
resource(kind) {
|
|
755
|
+
const def = resourceDef(kind);
|
|
756
|
+
const save = (request) => writeResource(request, def);
|
|
757
|
+
return {
|
|
758
|
+
GET: (request) => forward(request, { path: def.upstream }),
|
|
759
|
+
PUT: save,
|
|
760
|
+
POST: save
|
|
761
|
+
};
|
|
762
|
+
},
|
|
763
|
+
upload(options = {}) {
|
|
764
|
+
return { POST: (request) => uploadFile(request, options) };
|
|
648
765
|
}
|
|
649
766
|
};
|
|
650
767
|
}
|
|
@@ -971,6 +1088,11 @@ function createShopwaveAuth(config) {
|
|
|
971
1088
|
* export const { GET, PUT, DELETE } = shopwave.item("product");
|
|
972
1089
|
* // app/api/report/route.ts
|
|
973
1090
|
* export const { GET } = shopwave.passthrough("report");
|
|
1091
|
+
* // app/api/merchant/route.ts, app/api/user/route.ts
|
|
1092
|
+
* export const { GET, PUT } = shopwave.resource("merchant");
|
|
1093
|
+
* export const { GET } = shopwave.resource("user");
|
|
1094
|
+
* // app/api/upload/route.ts
|
|
1095
|
+
* export const { POST } = shopwave.upload();
|
|
974
1096
|
* ```
|
|
975
1097
|
*/
|
|
976
1098
|
function createShopwaveApi(config) {
|
|
@@ -983,5 +1105,5 @@ function createShopwaveApi(config) {
|
|
|
983
1105
|
}
|
|
984
1106
|
|
|
985
1107
|
//#endregion
|
|
986
|
-
export { SHOPWAVE_ENTITIES, ShopwaveAuthError, authorizationHeader, createShopwaveApi, createShopwaveAuth, isExpiredTokenResponse, readRequestToken };
|
|
1108
|
+
export { SHOPWAVE_ENTITIES, SHOPWAVE_RESOURCES, ShopwaveAuthError, UPLOAD_KINDS, authorizationHeader, createShopwaveApi, createShopwaveAuth, isExpiredTokenResponse, readRequestToken };
|
|
987
1109
|
//# sourceMappingURL=index.js.map
|