react-shopwave-connect 0.2.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 +215 -24
- package/dist/core/index.cjs +903 -88
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +604 -23
- package/dist/core/index.d.ts +604 -23
- package/dist/core/index.js +845 -89
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +735 -109
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +263 -12
- package/dist/hooks/index.d.ts +263 -12
- package/dist/hooks/index.js +732 -111
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +1087 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +670 -24
- package/dist/index.d.ts +670 -24
- package/dist/index.js +1025 -109
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +560 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +249 -1
- package/dist/next/index.d.ts +249 -1
- package/dist/next/index.js +556 -3
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +531 -1
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +258 -1
- package/dist/server/index.d.ts +258 -1
- package/dist/server/index.js +521 -2
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ Shopwave API client split into two clearly separated layers:
|
|
|
4
4
|
|
|
5
5
|
- **`core`** — framework-agnostic async API functions + all TypeScript types. No React anywhere. Works in any TS/JS project (Node script, Vue, Angular, CLI, …).
|
|
6
6
|
- **`hooks`** — thin React wrappers around `core`, with a consistent `{ data, loading, error, refetch }` shape. React is a **peer dependency** and is never bundled.
|
|
7
|
+
- **`server` / `next`** — the other half of the contract: OAuth login and the `/api/*` route handlers that `core` calls, so an app's API routes are one line each.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
browser / Node ──core──▶ your app's /api/<route> ──next route handlers──▶ Shopwave API
|
|
11
|
+
(session cookie or Authorization: OAuth <token>)
|
|
12
|
+
```
|
|
7
13
|
|
|
8
14
|
```
|
|
9
15
|
src/
|
|
@@ -29,7 +35,7 @@ npm install react react-dom
|
|
|
29
35
|
| `react-shopwave-connect/hooks` | React hooks only |
|
|
30
36
|
| `react-shopwave-connect` | Both (re-exports `core` + `hooks`) |
|
|
31
37
|
| `react-shopwave-connect/server` | **Server-only.** Framework-agnostic Shopwave OAuth client + token helpers |
|
|
32
|
-
| `react-shopwave-connect/next` | **Server-only.** Next.js login/callback/logout/session
|
|
38
|
+
| `react-shopwave-connect/next` | **Server-only.** Next.js login/callback/logout/session handlers, token refresh, proxy guard, and the `/api/*` route handlers (`createShopwaveApi`) |
|
|
33
39
|
|
|
34
40
|
Prefer the subpath imports when you want a hard boundary — e.g. a Node service should import from `/core` so React never enters the dependency graph.
|
|
35
41
|
|
|
@@ -41,13 +47,18 @@ Every `core` function (and every hook) accepts an optional `options` argument:
|
|
|
41
47
|
interface RequestOptions {
|
|
42
48
|
baseUrl?: string; // prefix for every path; omit in the browser to use
|
|
43
49
|
// relative URLs like "/api/products"
|
|
44
|
-
token?: string; //
|
|
50
|
+
token?: string; // sent as `Authorization: OAuth <token>` on every
|
|
51
|
+
// request (GET, POST, PUT, DELETE alike)
|
|
45
52
|
fetch?: typeof fetch; // custom fetch (Node < 18, tests, interceptors)
|
|
46
53
|
signal?: AbortSignal; // cancellation (hooks pass this automatically)
|
|
47
54
|
}
|
|
48
55
|
```
|
|
49
56
|
|
|
50
|
-
In the browser / Next.js you can usually omit `options` entirely (relative URLs resolve against the current origin). Outside the browser, pass
|
|
57
|
+
In the browser / Next.js you can usually omit `options` entirely (relative URLs resolve against the current origin, and the app's routes use the session cookie). Outside the browser, pass the absolute `baseUrl` of an app that mounts the SDK routes (see [API routes](#4-api-routes-nextjs)) and a `token`.
|
|
58
|
+
|
|
59
|
+
`core` always calls `/api/<route>` paths with the request metadata in an `extras` header, so `baseUrl` must point at such an app, not at the Shopwave API itself.
|
|
60
|
+
|
|
61
|
+
> **0.3 change:** the token used to travel in `extras.token` for reads and a separate `token` header for writes. It is now always the `Authorization` header. The 0.3 route handlers still accept both old forms, so older clients keep working.
|
|
51
62
|
|
|
52
63
|
---
|
|
53
64
|
|
|
@@ -64,8 +75,8 @@ import {
|
|
|
64
75
|
} from "react-shopwave-connect/core";
|
|
65
76
|
|
|
66
77
|
const options = {
|
|
67
|
-
baseUrl: "https://
|
|
68
|
-
token: process.env.SHOPWAVE_TOKEN,
|
|
78
|
+
baseUrl: "https://admin.example.com", // an app with the SDK's /api routes
|
|
79
|
+
token: process.env.SHOPWAVE_TOKEN, // bare access token
|
|
69
80
|
};
|
|
70
81
|
|
|
71
82
|
async function main() {
|
|
@@ -94,26 +105,139 @@ On Node < 18 (no global `fetch`), inject one:
|
|
|
94
105
|
import { fetchStores } from "react-shopwave-connect/core";
|
|
95
106
|
import fetch from "node-fetch";
|
|
96
107
|
|
|
97
|
-
await fetchStores({}, { baseUrl: "https://
|
|
108
|
+
await fetchStores({}, { baseUrl: "https://admin.example.com", token, fetch });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Functions **throw** `ShopwaveApiError` on failure:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { saveProduct, ShopwaveApiError } from "react-shopwave-connect/core";
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await saveProduct({ name: "Tea", barcode: "123" });
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (e instanceof ShopwaveApiError) {
|
|
120
|
+
e.status; // HTTP status (0 = no response). 200/201 when the API answered with errors.
|
|
121
|
+
e.errors; // api.message.errors, e.g. { 908: { id: 908, title: … } }
|
|
122
|
+
e.isUnauthorized; // 401 or error 908 → send the user to log in
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The SDK never logs requests (older versions `console.log`ged headers, including tokens).
|
|
128
|
+
|
|
129
|
+
### Saving and deleting (typed)
|
|
130
|
+
|
|
131
|
+
Each entity has `save…`, `delete…` and `fetch…(id)`:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { saveCategory, deleteCategory, fetchCategory } from "react-shopwave-connect/core";
|
|
135
|
+
|
|
136
|
+
const created = await saveCategory({ title: "Drinks", parentId: null }); // no id → create
|
|
137
|
+
created.id; // the new id, straight from the save
|
|
138
|
+
|
|
139
|
+
const updated = await saveCategory({ id: created.id, title: "Hot drinks" }); // id → update
|
|
140
|
+
|
|
141
|
+
await fetchCategory(created.id); // Category | null
|
|
142
|
+
await fetchCategory(created.id, { deleted: true }); // include soft-deleted
|
|
143
|
+
|
|
144
|
+
await deleteCategory(created.id);
|
|
98
145
|
```
|
|
99
146
|
|
|
100
|
-
|
|
147
|
+
| Entity | Save / delete / read one | App routes |
|
|
148
|
+
| --------- | --------------------------------------------------------- | ----------------------------- |
|
|
149
|
+
| product | `saveProduct`, `deleteProduct`, `fetchProduct` | `/api/products[/:id]` |
|
|
150
|
+
| category | `saveCategory`, `deleteCategory`, `fetchCategory` | `/api/categories[/:id]` |
|
|
151
|
+
| store | `saveStore`, `deleteStore`, `fetchStore` | `/api/stores[/:id]` |
|
|
152
|
+
| promotion | `savePromotion`, `deletePromotion` (ends it), `fetchPromotion` | `/api/promotions[/:id]` |
|
|
153
|
+
| employee | `saveEmployee`, `deleteEmployee` (retires), `fetchEmployee` | `/api/employees[/:id]` |
|
|
154
|
+
| consumer | `fetchConsumer` only — read-only in the API | `/api/consumer[/:id]` (GET) |
|
|
155
|
+
|
|
156
|
+
How they behave (matches what the Shopwave API does):
|
|
157
|
+
|
|
158
|
+
- **Save** sends `{ <collection>: { "0": entity } }`. Shopwave upserts (an `id` means update), answers **201** and echoes the entity under the same ref with its `id` and server fields. The function checks `api.message.errors` and that the ref came back, and returns the saved entity (fields the echo leaves out are kept from your input). `saveEntities(kind, [a, b])` saves several at once and matches results back by ref.
|
|
159
|
+
- **Delete** is a soft delete (read it back with `deleted: true`). Shopwave answers **205 with an empty body — also for ids that don't exist**, so a resolved promise means "accepted", not "a record was deleted".
|
|
160
|
+
- **Read one** calls `GET /api/<route>/:id` and returns the record or `null`. List reads return `[]` when nothing matches (Shopwave answers with an empty body).
|
|
161
|
+
- **Promotions:** Shopwave has no promotion DELETE; `deletePromotion` (and `DELETE /api/promotions/:id`) ends the promotion by setting `endDate` to now.
|
|
162
|
+
- **Employees:** Shopwave has no employee DELETE, so `deleteEmployee` (and `DELETE /api/employees/:id`) retires the employee by setting `exitDate`. Updating an existing employee only changes `roleId`, `joinedDate` and `exitDate` — names and email are fixed — and the echo carries only those fields, so re-read with `fetchEmployee` if you need the stored record.
|
|
163
|
+
- **Consumers** are read-only (`GET /consumer` by `ids`); their write/delete routes answer 405.
|
|
164
|
+
- Errors are read from `api.message.errors` and also `api.message.error` (the name used in the [API reference](https://developer.merchantstack.com/api-reference.html)).
|
|
165
|
+
|
|
166
|
+
The generic forms are `saveEntity(kind, item)`, `deleteEntityById(kind, id)` and `fetchEntityById(kind, id)`; `SHOPWAVE_ENTITIES` lists each entity's route, collection key, Shopwave path and id headers.
|
|
167
|
+
|
|
168
|
+
### Merchant, current user and image uploads
|
|
169
|
+
|
|
170
|
+
The merchant and the logged-in user are single records (one per login), and images go to the Shopwave CDN:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import {
|
|
174
|
+
fetchMerchant, updateMerchant, fetchUser,
|
|
175
|
+
uploadMerchantImage, setMerchantImage, getMerchantImageUrl,
|
|
176
|
+
merchantLinksToForm, merchantLinksFromForm,
|
|
177
|
+
} from "react-shopwave-connect/core";
|
|
178
|
+
|
|
179
|
+
const merchant = await fetchMerchant(); // Merchant | null
|
|
180
|
+
const user = await fetchUser(); // User | null ({ id, firstName, lastName, email, employee })
|
|
181
|
+
|
|
182
|
+
// Reads the merchant, merges the patch over it and saves the record, so
|
|
183
|
+
// fields you don't mention (companyNumber, vatNumber, links, …) are kept.
|
|
184
|
+
// Returns the merchant as stored (read back after the save).
|
|
185
|
+
await updateMerchant({ vatNumber: "123456789", links: merchantLinksFromForm({ website: "https://example.com" }) });
|
|
186
|
+
|
|
187
|
+
// Images: fit to the slot's size, upload with the slot's kind, then save the file name.
|
|
188
|
+
const image = await uploadMerchantImage(file, "square"); // { id, url, path }
|
|
189
|
+
await updateMerchant({ imageIds: setMerchantImage(merchant?.imageIds, "square", image.id) });
|
|
190
|
+
getMerchantImageUrl(saved.imageIds, "square"); // HTTPS URL for <img src>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The shapes below are the ones adminV1 saves and adminV2 reads, so all three admins share one record (confirmed on the live API with merchant 5644, 30 Sep 2026):
|
|
194
|
+
|
|
195
|
+
| Field | Shape |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `imageIds` | `{ logo: { receipt, square }, feature: [ … ] }` — **file names** on write (the uploader's `fileName`), **full `http://static.merchantstack.com/…` URLs** on read |
|
|
198
|
+
| `colours` | `{ primary: { main, highlight, contrast } }` (hex strings; adminV2 uses `primary.main` behind the logo) |
|
|
199
|
+
| `links` | `{ website: { home }, social: { twitter, facebook, instagram } }` |
|
|
200
|
+
|
|
201
|
+
- **Merchant fields** (`GET`/`POST /merchant`): `id`, `name`, `description`, `companyNumber`, `vatNumber`, `categoryId`, `estAnnualRevenue`, `note`, and the three objects above. The API returns `{}` for an empty object.
|
|
202
|
+
- **What the live API does on `POST /merchant`** (checked 30 Sep 2026): it replaces the record. A scalar field left out (`companyNumber`, `vatNumber`, `note`, …) becomes `null`; an object field left out (`colours`, `links`, `imageIds`) is kept, and one that is sent replaces the stored one (`{}` clears it). It answers **205 with an empty body**. It silently cuts `companyNumber` to 11 characters and `vatNumber` to 9 (a UK VAT number without `GB`).
|
|
203
|
+
- **`updateMerchant(patch, { merge })`**: `merge` (default `true`) reads first and merges the scalar fields, so nothing is wiped. Object fields (`MERCHANT_OBJECT_FIELDS`) are sent only when the patch has them, and then replace the stored one — to change one image, pass the whole `imageIds` (`setMerchantImage` does that). `imageIds` is always sent as file names (`toStoredImageIds`), so you can pass back what you read. It refuses a different merchant `id`, never sends `createdDate`/`modifiedDate`, rejects values longer than `MERCHANT_FIELD_MAX_LENGTH` (`validateMerchant(m)` lists them) instead of letting the API cut them, and returns the merchant **read back after the save**, so you see exactly what was kept. `merge: false` sends only the patch (which must include `id`, and clears the scalar fields you leave out).
|
|
204
|
+
- **Merchant image slots** (`MERCHANT_IMAGE_SLOTS`): the API doesn't resize, and adminV1 accepts only these exact sizes.
|
|
205
|
+
|
|
206
|
+
| Slot | Stored at | Upload kind | Size |
|
|
207
|
+
|---|---|---|---|
|
|
208
|
+
| `receipt` | `imageIds.logo.receipt` | `merchant` | 576 × 325 |
|
|
209
|
+
| `square` | `imageIds.logo.square` | `merchant` | 1000 × 1000 |
|
|
210
|
+
| `featured` | `imageIds.feature[0]` | `merchantFeature` | 1200 × 600 |
|
|
211
|
+
|
|
212
|
+
The API builds each image URL from its key, not from where the file was uploaded (`logo.*` → `…/merchant/<id>/logo/<file>`, `feature` → `…/feature/<file>`), so the featured image must be uploaded as `merchantFeature`. `POST /merchant` stores all three objects with the SDK's `{ merchant: … }` body; adminV1's `{ merchants: { "0": … } }` form isn't needed.
|
|
213
|
+
|
|
214
|
+
`uploadMerchantImage(file, slot, { fit = true })` scales and centre-crops the image to the slot's size in the browser (`fitImageToSize`, `coverCrop`) and uploads it with the slot's kind; `fit: false` sends the file as-is. `getMerchantImage` / `getMerchantImageUrl` / `setMerchantImage` read and write a slot; `imageFileName(url)` gives the file name in a URL. Hook: `useUploadMerchantImage()`.
|
|
215
|
+
- **Links:** `merchantLinksToForm(links)` gives flat fields (`website`, `twitter`, `facebook`, `instagram`) from the stored shape, flat keys or the old array form; `merchantLinksFromForm(fields)` builds the stored shape; `normalizeMerchantLinks(links)` converts any of them to the stored shape. Colour keys: `MERCHANT_COLOUR_KEYS`.
|
|
216
|
+
- **User** (`GET /user`): the live API returns `id`, `firstName`, `lastName`, `email` and `employee: { merchantId, roleId, stores }` — no `createdDate` and **no `merchant`**, although the reference lists them. There's no option to include it (adminV1 also reads `GET /merchant` separately and attaches it to its session). Read the merchant with `fetchMerchant()`.
|
|
217
|
+
- **`uploadImage(file, { kind, fileName? })`** posts the file to `/api/upload`, which `PUT`s it to Shopwave's `/uploader` (multipart `file`, header `contentType: <kind>`). Kinds: `merchant`, `merchantFeature`, `product`, `user`, `applicationLogo`, `applicationImages` (the last two sent as the API's own spelling, `applicaionLogo`/`applicaionImages`). Live: 201 `{ fileName, path }`. Returns `{ id, url, path }`: `id` is the generated file name (what records store), `path` the URL as the API gave it (`http://static.merchantstack.com/images/…`, which has no HTTPS endpoint), and `url` the same file over HTTPS via `getImageUrl(path)` (`SHOPWAVE_IMAGE_BASE_URL`, the S3 bucket behind the CDN). The upload alone changes no record.
|
|
218
|
+
|
|
219
|
+
`SHOPWAVE_RESOURCES` (merchant, user) and `SHOPWAVE_UPLOAD` are shared with the server routes, like `SHOPWAVE_ENTITIES`.
|
|
101
220
|
|
|
102
221
|
### Available `core` functions
|
|
103
222
|
|
|
104
223
|
| Domain | Function(s) |
|
|
105
224
|
| --------- | ------------------------------------------------------- |
|
|
106
|
-
| category | `fetchCategories`
|
|
107
|
-
| consumer | `fetchConsumers`
|
|
108
|
-
| employee | `fetchEmployees`
|
|
109
|
-
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed)
|
|
110
|
-
|
|
|
225
|
+
| category | `fetchCategories`, `fetchCategory`, `saveCategory`, `deleteCategory` |
|
|
226
|
+
| consumer | `fetchConsumers`, `fetchConsumer` (read-only) |
|
|
227
|
+
| employee | `fetchEmployees`, `fetchEmployee`, `saveEmployee`, `deleteEmployee` |
|
|
228
|
+
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct` |
|
|
229
|
+
| promotion | `fetchPromotions`, `fetchPromotion`, `savePromotion`, `deletePromotion` |
|
|
230
|
+
| store | `fetchStores` (now with `storeIds`), `fetchStore`, `saveStore`, `deleteStore` |
|
|
111
231
|
| report | `fetchReport` |
|
|
232
|
+
| merchant | `fetchMerchant`, `updateMerchant`, `validateMerchant`; links `merchantLinksToForm`, `merchantLinksFromForm`, `normalizeMerchantLinks`; images `MERCHANT_IMAGE_SLOTS`, `uploadMerchantImage`, `getMerchantImage`, `getMerchantImageUrl`, `setMerchantImage`, `toStoredImageIds`, `imageFileName` |
|
|
233
|
+
| user | `fetchUser` |
|
|
234
|
+
| upload | `uploadImage`, `getImageUrl`, `fitImageToSize`, `readImageSize`, `coverCrop` |
|
|
112
235
|
| session | `fetchSession`, `loginPath`, `logoutPath`, `logout` |
|
|
113
|
-
| entity | `
|
|
236
|
+
| entity | `saveEntity`, `saveEntities`, `deleteEntityById`, `fetchEntityById`; low-level `submitEntity` / `deleteEntity` (raw endpoint) |
|
|
237
|
+
| errors | `ShopwaveApiError`, `getApiErrorMap`, `assertNoApiErrors` |
|
|
114
238
|
| basket | `buildBasketReportQuery`, `parseBasketReportData`, `combineBasketRows`, `computeBasketSummary`, … (pure transforms) |
|
|
115
239
|
|
|
116
|
-
All types/interfaces (`Product`, `Store`, `Category`, `Consumer`, `Employee`, `ReportQueryMap`, `Basket*`, `apiResponse`, …) are exported from `core` too.
|
|
240
|
+
All types/interfaces (`Product`, `Store`, `Category`, `Consumer`, `Employee`, `Merchant`, `User`, `UploadedImage`, `ReportQueryMap`, `Basket*`, `apiResponse`, …) are exported from `core` too.
|
|
117
241
|
|
|
118
242
|
---
|
|
119
243
|
|
|
@@ -122,9 +246,13 @@ All types/interfaces (`Product`, `Store`, `Category`, `Consumer`, `Employee`, `R
|
|
|
122
246
|
Every auto-fetching hook returns the same shape:
|
|
123
247
|
|
|
124
248
|
```ts
|
|
125
|
-
{ data, loading, error, refetch }
|
|
249
|
+
{ data, loading, fetching, error, errorStatus, refetch }
|
|
126
250
|
```
|
|
127
251
|
|
|
252
|
+
- `loading` is true only while there's nothing to show yet (first load, or the params changed).
|
|
253
|
+
- `refetch()` keeps the current `data` on screen until the new data arrives (`fetching` is true meanwhile), so lists don't flash a skeleton after every save. A failed refetch keeps the old data and sets `error`.
|
|
254
|
+
- `errorStatus` is the HTTP status of the last error.
|
|
255
|
+
|
|
128
256
|
```tsx
|
|
129
257
|
"use client";
|
|
130
258
|
import { useProduct, useStore } from "react-shopwave-connect/hooks";
|
|
@@ -152,12 +280,14 @@ const { data } = useProduct({ storeId }, { baseUrl: "https://api.merchantstack.c
|
|
|
152
280
|
|
|
153
281
|
### Auto-fetch hooks (`useEffect`-based)
|
|
154
282
|
|
|
155
|
-
`useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession
|
|
283
|
+
`useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession`, `useMerchant`, `useUser` (`useUser({ enabled: false })` stays idle).
|
|
156
284
|
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.
|
|
157
285
|
|
|
158
286
|
### Manually-triggered hooks (`useCallback`-based)
|
|
159
287
|
|
|
160
|
-
|
|
288
|
+
For saves and deletes, prefer calling the typed `core` functions (`saveProduct`, `deleteStore`, …) from your event handlers — they return the saved entity with its id.
|
|
289
|
+
|
|
290
|
+
`useUpdateMerchant`, `useUploadImage`, `useUploadMerchantImage`, `useDelete`, `useSubmit`, `useLogout` return `{ mutate, data, loading, error, errorStatus }` — nothing fires until you call `mutate`:
|
|
161
291
|
|
|
162
292
|
```tsx
|
|
163
293
|
import { useDelete, useSubmit } from "react-shopwave-connect/hooks";
|
|
@@ -303,9 +433,65 @@ export function AccountButton() {
|
|
|
303
433
|
|
|
304
434
|
Use plain links / `window.location` for login and logout — they are full-page redirects to the auth server, not client-side navigations.
|
|
305
435
|
|
|
436
|
+
### 4. API routes (Next.js)
|
|
437
|
+
|
|
438
|
+
`core` calls `/api/<route>` on your app. `createShopwaveApi` provides those routes, using the session from step 1:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
// lib/shopwave.ts
|
|
442
|
+
import { createShopwaveApi } from "react-shopwave-connect/next";
|
|
443
|
+
import { auth } from "@/lib/auth";
|
|
444
|
+
|
|
445
|
+
export const shopwave = createShopwaveApi({ auth, apiUrl: process.env.SHOPWAVE_API_SERVER_URL! });
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
// app/api/products/route.ts — list/filter, create/update
|
|
450
|
+
import { shopwave } from "@/lib/shopwave";
|
|
451
|
+
export const { GET, POST, PUT } = shopwave.collection("product");
|
|
452
|
+
|
|
453
|
+
// app/api/products/[id]/route.ts — read one, update, delete
|
|
454
|
+
import { shopwave } from "@/lib/shopwave";
|
|
455
|
+
export const { GET, PUT, DELETE } = shopwave.item("product");
|
|
456
|
+
|
|
457
|
+
// app/api/report/route.ts — any other Shopwave path, read-only
|
|
458
|
+
import { shopwave } from "@/lib/shopwave";
|
|
459
|
+
export const { GET } = shopwave.passthrough("report");
|
|
460
|
+
|
|
461
|
+
// app/api/merchant/route.ts — read / update the merchant
|
|
462
|
+
export const { GET, PUT } = shopwave.resource("merchant");
|
|
463
|
+
|
|
464
|
+
// app/api/user/route.ts — the logged-in user (read-only)
|
|
465
|
+
export const { GET } = shopwave.resource("user");
|
|
466
|
+
|
|
467
|
+
// app/api/upload/route.ts — image upload (uploadImage)
|
|
468
|
+
export const { POST } = shopwave.upload(); // or upload({ maxBytes, accept })
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
- `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.
|
|
472
|
+
- `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.
|
|
473
|
+
|
|
474
|
+
Entities: `product`, `category`, `store`, `promotion`, `employee` (DELETE retires via `exitDate`), `promotion` (DELETE ends it via `endDate`), `consumer` (mounted at `/api/consumer`; GET only, writes answer 405). For anything else, `shopwave.forward(request, { method, path, headers, postBody })` does the same token/refresh/encoding work for a custom handler.
|
|
475
|
+
|
|
476
|
+
Every handler:
|
|
477
|
+
|
|
478
|
+
- uses the caller's `Authorization: OAuth <token>` when present (also the legacy `token` header / `extras.token`), otherwise the logged-in user's token; `401` when there's neither. Turn caller tokens off with `allowRequestToken: false`.
|
|
479
|
+
- turns the `extras` JSON header into upstream headers, dropping `Authorization`, `token`, `Cookie`, `Host`, `x-accept-version` and other transport headers; `400` if it isn't a JSON object.
|
|
480
|
+
- sends writes as the form field `postBody=<JSON>`, accepting the SDK shape `{ products: { "0": {…} } }`, the older `{ products: { new | updated: {…} } }`, or a bare entity. Item `PUT` forces the id from the URL.
|
|
481
|
+
- on HTTP 401 or API error 908 with the session token, refreshes once and retries.
|
|
482
|
+
- answers with Shopwave's own status and body (201 + echo for saves, 205 + empty body for deletes, error envelopes as-is); `502` if the API can't be reached. Only the method and path are ever logged.
|
|
483
|
+
|
|
484
|
+
For calls that bring their own token (scripts, the integration tests), let them past the proxy guard:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
await auth.protect(request, { publicPaths: [...], allowRequestToken: true });
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Options: `apiVersion` (default `"2.0"`), `fetch`, `entities` (override a route/header, e.g. `{ employee: { idHeader: "userId" } }`), `blockedExtras`, `onError`.
|
|
491
|
+
|
|
306
492
|
### Other frameworks
|
|
307
493
|
|
|
308
|
-
`react-shopwave-connect/server` has the same building blocks without Next.js: `createShopwaveOAuth(config)` → `buildLoginUrl({ state })`, `exchangeCode(code)`, `refreshToken(token)`, `buildLogoutUrl()`, plus `createState`, `sanitizeReturnTo`, `isTokenExpired`, `isExpiredTokenResponse` and `authorizationHeader`. Store the token in your framework's session (Express `express-session`, iron-session's `getIronSession(req, res)`, …).
|
|
494
|
+
`react-shopwave-connect/server` has the same building blocks without Next.js: `createShopwaveOAuth(config)` → `buildLoginUrl({ state })`, `exchangeCode(code)`, `refreshToken(token)`, `buildLogoutUrl()`, plus `createState`, `sanitizeReturnTo`, `isTokenExpired`, `isExpiredTokenResponse` and `authorizationHeader`. The API routes are `createShopwaveApiHandlers({ apiUrl, getAuthorization })` — plain `Request → Response` handlers, the same ones `createShopwaveApi` wraps. Store the token in your framework's session (Express `express-session`, iron-session's `getIronSession(req, res)`, …).
|
|
309
495
|
|
|
310
496
|
### Moving an existing app over
|
|
311
497
|
|
|
@@ -329,27 +515,28 @@ npm run typecheck # tsc --noEmit
|
|
|
329
515
|
|
|
330
516
|
## Testing
|
|
331
517
|
|
|
332
|
-
Unit tests (offline, no credentials) cover the OAuth client, token handling, the Next.js handlers (with an in-memory cookie store) and the
|
|
518
|
+
Unit tests (offline, no credentials) cover the OAuth client, token handling, the Next.js handlers (with an in-memory cookie store), the session client, the request layer (token header, errors, no logging), the typed save/delete/read functions, the API route handlers (extras filtering, refresh-and-retry, body normalisation, 205 pass-through) and the hooks' query state:
|
|
333
519
|
|
|
334
520
|
```bash
|
|
335
521
|
npm test # = npm run test:unit
|
|
336
522
|
```
|
|
337
523
|
|
|
338
|
-
Integration tests
|
|
524
|
+
Integration tests use [Vitest](https://vitest.dev) and run against **an app that mounts the SDK routes** (e.g. AdminUI on `npm run dev`), which forwards to the Shopwave API. They cover create → read (by id and by filter) → update → delete with the typed functions for Products and Categories, create → read → role update → retire for Employees, read-only checks for Consumers, plus unknown ids and the legacy `submitEntity` body. They create and delete real records on the account behind the token.
|
|
339
525
|
|
|
340
526
|
### Environment Variables
|
|
341
527
|
|
|
342
528
|
| Variable | Description | Example |
|
|
343
529
|
|-------------|--------------------------------------|-------------------------------------------|
|
|
344
|
-
| `API_URL` |
|
|
345
|
-
| `API_TOKEN` |
|
|
530
|
+
| `API_URL` | Origin of the app with the SDK routes | `http://localhost:3000` |
|
|
531
|
+
| `API_TOKEN` | Bare Shopwave access token (no `OAuth`/`Bearer` prefix) | `111ad…` |
|
|
346
532
|
|
|
347
533
|
### Running Tests
|
|
348
534
|
|
|
349
535
|
```bash
|
|
350
536
|
# Set environment variables
|
|
351
|
-
|
|
352
|
-
export
|
|
537
|
+
# with the app running (AdminUI: npm run dev)
|
|
538
|
+
export API_URL=http://localhost:3000
|
|
539
|
+
export API_TOKEN=<bare access token>
|
|
353
540
|
|
|
354
541
|
# Run all integration tests
|
|
355
542
|
npm run test:integration
|
|
@@ -366,6 +553,10 @@ tests/
|
|
|
366
553
|
server.test.ts # OAuth client, tokens, returnTo/state
|
|
367
554
|
next.test.ts # Next.js handlers, refresh, proxy guard
|
|
368
555
|
session-client.test.ts # fetchSession / loginPath / logoutPath
|
|
556
|
+
request.test.ts # token header, ShopwaveApiError, no logging
|
|
557
|
+
entities.test.ts # save/delete/fetch-by-id per entity
|
|
558
|
+
api-routes.test.ts # /api route handlers
|
|
559
|
+
query-state.test.ts # useQuery keeps data while refetching
|
|
369
560
|
integration/
|
|
370
561
|
setup.ts # Shared config and helpers
|
|
371
562
|
products.integration.test.ts # Products CRUD lifecycle
|