react-shopwave-connect 0.1.2 → 0.3.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 +281 -39
- package/dist/core/index.cjs +435 -93
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +241 -28
- package/dist/core/index.d.ts +241 -28
- package/dist/core/index.js +405 -94
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +328 -116
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +57 -12
- package/dist/hooks/index.d.ts +57 -12
- package/dist/hooks/index.js +329 -118
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +553 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -29
- package/dist/index.d.ts +265 -29
- package/dist/index.js +523 -114
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +1019 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +423 -0
- package/dist/next/index.d.ts +423 -0
- package/dist/next/index.js +987 -0
- package/dist/next/index.js.map +1 -0
- package/dist/server/index.cjs +670 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +346 -0
- package/dist/server/index.d.ts +346 -0
- package/dist/server/index.js +650 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +41 -8
package/dist/hooks/index.cjs
CHANGED
|
@@ -24,89 +24,180 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
let react = require("react");
|
|
25
25
|
react = __toESM(react);
|
|
26
26
|
|
|
27
|
+
//#region src/core/errors.ts
|
|
28
|
+
/**
|
|
29
|
+
* Thrown by every `core` function when a request fails, either at the HTTP
|
|
30
|
+
* level (`status` is the HTTP status) or because the Shopwave response carried
|
|
31
|
+
* `api.message.errors` (then `status` is the HTTP status of that response,
|
|
32
|
+
* usually 200/201, and `errors` holds the API errors).
|
|
33
|
+
*
|
|
34
|
+
* Branch on `status` (e.g. `isUnauthorized` → send the user to log in) rather
|
|
35
|
+
* than parsing `message`.
|
|
36
|
+
*/
|
|
37
|
+
var ShopwaveApiError = class extends Error {
|
|
38
|
+
/** HTTP status of the response (0 when the request never got a response). */
|
|
39
|
+
status;
|
|
40
|
+
/** Shopwave API errors, when the response carried any. */
|
|
41
|
+
errors;
|
|
42
|
+
/** Parsed response body, when there was one. */
|
|
43
|
+
body;
|
|
44
|
+
constructor(status, message, details = {}) {
|
|
45
|
+
super(message, details.cause !== void 0 ? { cause: details.cause } : void 0);
|
|
46
|
+
this.name = "ShopwaveApiError";
|
|
47
|
+
this.status = status;
|
|
48
|
+
this.errors = details.errors ?? null;
|
|
49
|
+
this.body = details.body;
|
|
50
|
+
}
|
|
51
|
+
/** 401, or the Shopwave "token expired / invalid" error 908. */
|
|
52
|
+
get isUnauthorized() {
|
|
53
|
+
if (this.status === 401) return true;
|
|
54
|
+
return !!this.errors && Object.entries(this.errors).some(([k, v]) => k === "908" || Number(v?.id) === 908);
|
|
55
|
+
}
|
|
56
|
+
get isNotFound() {
|
|
57
|
+
return this.status === 404;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/** The `api.message.errors` (or `api.message.error`) map of a Shopwave response, or `null` when there are none. */
|
|
61
|
+
function getApiErrorMap(body) {
|
|
62
|
+
const message = body?.api?.message;
|
|
63
|
+
const errors = message?.errors ?? message?.error;
|
|
64
|
+
if (errors && typeof errors === "object" && Object.keys(errors).length > 0) return errors;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function describeErrors(errors) {
|
|
68
|
+
return Object.entries(errors).map(([code, e]) => {
|
|
69
|
+
const text = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
70
|
+
return text ? `${code} ${text}` : code;
|
|
71
|
+
}).join("; ");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Builds a {@link ShopwaveApiError} for a response. Understands the Shopwave
|
|
75
|
+
* envelope (`api.message.errors`) and the `{ error, message }` bodies returned
|
|
76
|
+
* by the `react-shopwave-connect/next` routes.
|
|
77
|
+
*/
|
|
78
|
+
function toShopwaveApiError(status, body, fallback) {
|
|
79
|
+
const errors = getApiErrorMap(body);
|
|
80
|
+
let detail = errors ? describeErrors(errors) : "";
|
|
81
|
+
if (!detail && body && typeof body === "object") {
|
|
82
|
+
const b = body;
|
|
83
|
+
if (typeof b.message === "string") detail = b.message;
|
|
84
|
+
else if (typeof b.error === "string") detail = b.error;
|
|
85
|
+
}
|
|
86
|
+
if (!detail) detail = fallback ?? (status >= 400 ? "request failed" : "API returned errors");
|
|
87
|
+
return new ShopwaveApiError(status, `Shopwave API error (${status}): ${detail}`, {
|
|
88
|
+
errors,
|
|
89
|
+
body
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Throws a {@link ShopwaveApiError} when the response body carries API errors. */
|
|
93
|
+
function assertNoApiErrors(body, status) {
|
|
94
|
+
if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
27
98
|
//#region src/core/request.ts
|
|
28
99
|
function resolveFetch(options) {
|
|
29
100
|
const f = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
30
101
|
if (!f) throw new Error("No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.");
|
|
31
102
|
return f;
|
|
32
103
|
}
|
|
33
|
-
|
|
104
|
+
/** Options with `token` filled in from a legacy per-call `params.token`, unless `options.token` is set. */
|
|
105
|
+
function withToken(options, token) {
|
|
106
|
+
return token && !options.token ? {
|
|
107
|
+
...options,
|
|
108
|
+
token
|
|
109
|
+
} : options;
|
|
110
|
+
}
|
|
111
|
+
/** `Authorization` header value for a token (`OAuth <token>` unless it already has a scheme). */
|
|
112
|
+
function authorizationFor(token) {
|
|
113
|
+
const t = token.trim();
|
|
114
|
+
return /^(oauth|bearer)\s+\S/i.test(t) ? t : `OAuth ${t}`;
|
|
115
|
+
}
|
|
116
|
+
/** Headers shared by every request: Accept + the token, when one is given. */
|
|
117
|
+
function baseHeaders(options) {
|
|
118
|
+
const headers = { Accept: "application/json" };
|
|
119
|
+
if (options.token) headers.Authorization = authorizationFor(options.token);
|
|
120
|
+
return headers;
|
|
121
|
+
}
|
|
122
|
+
function buildExtras(extras) {
|
|
34
123
|
const merged = {
|
|
35
124
|
"Content-Type": "application/json",
|
|
36
125
|
...extras
|
|
37
126
|
};
|
|
38
|
-
|
|
127
|
+
delete merged.token;
|
|
39
128
|
return merged;
|
|
40
129
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
130
|
+
/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
|
|
131
|
+
async function readBody(response) {
|
|
132
|
+
const text = await response.text();
|
|
133
|
+
if (!text) return null;
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(text);
|
|
136
|
+
} catch {
|
|
137
|
+
return { raw: text };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function send(path, init, options) {
|
|
141
|
+
const doFetch = resolveFetch(options);
|
|
142
|
+
const url = (options.baseUrl ?? "") + path;
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
response = await doFetch(url, {
|
|
146
|
+
...init,
|
|
147
|
+
signal: options.signal
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error?.name === "AbortError") throw error;
|
|
151
|
+
throw new ShopwaveApiError(0, `Shopwave API request failed: ${error?.message ?? error}`, { cause: error });
|
|
152
|
+
}
|
|
153
|
+
const body = await readBody(response);
|
|
154
|
+
if (!response.ok) throw toShopwaveApiError(response.status, body, response.statusText || void 0);
|
|
155
|
+
return {
|
|
156
|
+
status: response.status,
|
|
157
|
+
body
|
|
158
|
+
};
|
|
49
159
|
}
|
|
50
160
|
/**
|
|
51
|
-
* GET helper. Sends request metadata via the `extras` header
|
|
52
|
-
*
|
|
161
|
+
* GET helper. Sends request metadata via the `extras` header, parses JSON and
|
|
162
|
+
* returns the payload. Throws {@link ShopwaveApiError} for non-2xx responses.
|
|
53
163
|
*/
|
|
54
164
|
async function apiGet(path, extras, options = {}) {
|
|
55
|
-
const
|
|
56
|
-
const url = (options.baseUrl ?? "") + path;
|
|
57
|
-
const headers = { extras: JSON.stringify(buildExtras(extras, options)) };
|
|
58
|
-
console.log("[API GET]", {
|
|
59
|
-
url,
|
|
60
|
-
headers
|
|
61
|
-
});
|
|
62
|
-
const response = await doFetch(url, {
|
|
165
|
+
const { body } = await send(path, {
|
|
63
166
|
method: "GET",
|
|
64
|
-
headers
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
167
|
+
headers: {
|
|
168
|
+
...baseHeaders(options),
|
|
169
|
+
extras: JSON.stringify(buildExtras(extras))
|
|
170
|
+
}
|
|
171
|
+
}, options);
|
|
172
|
+
return body;
|
|
69
173
|
}
|
|
70
174
|
/**
|
|
71
|
-
* JSON body helper for POST/PUT mutations.
|
|
175
|
+
* JSON body helper for POST/PUT mutations. Returns the parsed body (`null` for
|
|
176
|
+
* an empty one). Throws {@link ShopwaveApiError} for non-2xx responses.
|
|
72
177
|
*/
|
|
73
178
|
async function apiSend(path, method, payload, options = {}) {
|
|
74
|
-
const
|
|
75
|
-
const url = (options.baseUrl ?? "") + path;
|
|
76
|
-
const headers = { "Content-Type": "application/json" };
|
|
77
|
-
if (options.token != null) headers.token = options.token;
|
|
78
|
-
console.log(`[API ${method}]`, {
|
|
79
|
-
url,
|
|
80
|
-
headers,
|
|
81
|
-
payload
|
|
82
|
-
});
|
|
83
|
-
const response = await doFetch(url, {
|
|
179
|
+
const { body } = await send(path, {
|
|
84
180
|
method,
|
|
85
|
-
headers
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
181
|
+
headers: {
|
|
182
|
+
...baseHeaders(options),
|
|
183
|
+
"Content-Type": "application/json"
|
|
184
|
+
},
|
|
185
|
+
body: JSON.stringify(payload)
|
|
186
|
+
}, options);
|
|
187
|
+
return body;
|
|
91
188
|
}
|
|
92
189
|
/**
|
|
93
|
-
* DELETE helper.
|
|
190
|
+
* DELETE helper. Shopwave answers deletes with 205 and an empty body; if a body
|
|
191
|
+
* does come back and carries API errors, they are thrown. Returns the parsed
|
|
192
|
+
* body (`null` when empty).
|
|
94
193
|
*/
|
|
95
194
|
async function apiDelete(path, options = {}) {
|
|
96
|
-
const
|
|
97
|
-
const url = (options.baseUrl ?? "") + path;
|
|
98
|
-
const headers = {};
|
|
99
|
-
if (options.token != null) headers.token = options.token;
|
|
100
|
-
console.log("[API DELETE]", {
|
|
101
|
-
url,
|
|
102
|
-
headers
|
|
103
|
-
});
|
|
104
|
-
const response = await doFetch(url, {
|
|
195
|
+
const { status, body } = await send(path, {
|
|
105
196
|
method: "DELETE",
|
|
106
|
-
headers
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
197
|
+
headers: baseHeaders(options)
|
|
198
|
+
}, options);
|
|
199
|
+
if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
|
|
200
|
+
return body;
|
|
110
201
|
}
|
|
111
202
|
|
|
112
203
|
//#endregion
|
|
@@ -118,11 +209,9 @@ async function apiDelete(path, options = {}) {
|
|
|
118
209
|
async function fetchCategories(params = {}, options = {}) {
|
|
119
210
|
const extras = { deleted: params.deleted ?? false };
|
|
120
211
|
if (params.categoryIds) extras.categoryIds = params.categoryIds;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (error) throw new Error(error);
|
|
125
|
-
return Object.values(json.categories ?? {});
|
|
212
|
+
const json = await apiGet("/api/categories", extras, withToken(options, params.token));
|
|
213
|
+
assertNoApiErrors(json, 200);
|
|
214
|
+
return Object.values(json?.categories ?? {});
|
|
126
215
|
}
|
|
127
216
|
|
|
128
217
|
//#endregion
|
|
@@ -134,9 +223,8 @@ async function fetchCategories(params = {}, options = {}) {
|
|
|
134
223
|
async function fetchConsumers(consumerIds, options = {}) {
|
|
135
224
|
if (!consumerIds || consumerIds.length === 0) return [];
|
|
136
225
|
const json = await apiGet("/api/consumer", { ids: consumerIds.join(",") }, options);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return Object.values(json.consumers ?? {});
|
|
226
|
+
assertNoApiErrors(json, 200);
|
|
227
|
+
return Object.values(json?.consumers ?? {});
|
|
140
228
|
}
|
|
141
229
|
|
|
142
230
|
//#endregion
|
|
@@ -147,10 +235,9 @@ async function fetchConsumers(consumerIds, options = {}) {
|
|
|
147
235
|
async function fetchEmployees(params = {}, options = {}) {
|
|
148
236
|
const extras = { deleted: params.deleted ?? false };
|
|
149
237
|
if (params.employeeIds) extras.employeeIds = params.employeeIds;
|
|
150
|
-
const json = await apiGet("/api/employees", extras, options);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return Object.values(json.employees ?? {});
|
|
238
|
+
const json = await apiGet("/api/employees", extras, withToken(options, params.token));
|
|
239
|
+
assertNoApiErrors(json, 200);
|
|
240
|
+
return Object.values(json?.employees ?? {});
|
|
154
241
|
}
|
|
155
242
|
|
|
156
243
|
//#endregion
|
|
@@ -162,11 +249,9 @@ async function fetchProducts(params = {}, options = {}) {
|
|
|
162
249
|
const extras = { deleted: params.deleted ?? false };
|
|
163
250
|
if (params.productIds) extras.productIds = params.productIds;
|
|
164
251
|
if (params.storeId) extras.storeId = params.storeId;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (error) throw new Error(error);
|
|
169
|
-
return Object.values(json.products ?? {});
|
|
252
|
+
const json = await apiGet("/api/products", extras, withToken(options, params.token));
|
|
253
|
+
assertNoApiErrors(json, 200);
|
|
254
|
+
return Object.values(json?.products ?? {});
|
|
170
255
|
}
|
|
171
256
|
/**
|
|
172
257
|
* Fetches products by id and returns them keyed by id, transparently batching
|
|
@@ -195,11 +280,9 @@ async function fetchPromotions(params = {}, options = {}) {
|
|
|
195
280
|
if (params.imminent !== void 0) extras.imminent = params.imminent;
|
|
196
281
|
if (params.active !== void 0) extras.active = params.active;
|
|
197
282
|
if (params.expired !== void 0) extras.expired = params.expired;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (error) throw new Error(error);
|
|
202
|
-
return Object.values(json.promotions ?? {});
|
|
283
|
+
const json = await apiGet("/api/promotions", extras, withToken(options, params.token));
|
|
284
|
+
assertNoApiErrors(json, 200);
|
|
285
|
+
return Object.values(json?.promotions ?? {});
|
|
203
286
|
}
|
|
204
287
|
|
|
205
288
|
//#endregion
|
|
@@ -208,10 +291,11 @@ async function fetchPromotions(params = {}, options = {}) {
|
|
|
208
291
|
* Fetches stores. Returns the flattened `Store[]`.
|
|
209
292
|
*/
|
|
210
293
|
async function fetchStores(params = {}, options = {}) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
294
|
+
const extras = { deleted: params.deleted ?? false };
|
|
295
|
+
if (params.storeIds) extras.storeIds = params.storeIds;
|
|
296
|
+
const json = await apiGet("/api/stores", extras, withToken(options, params.token));
|
|
297
|
+
assertNoApiErrors(json, 200);
|
|
298
|
+
return Object.values(json?.stores ?? {});
|
|
215
299
|
}
|
|
216
300
|
|
|
217
301
|
//#endregion
|
|
@@ -223,20 +307,43 @@ async function fetchStores(params = {}, options = {}) {
|
|
|
223
307
|
*/
|
|
224
308
|
async function fetchReport(query, options = {}) {
|
|
225
309
|
const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return json.reports ?? {};
|
|
310
|
+
assertNoApiErrors(json, 200);
|
|
311
|
+
return json?.reports ?? {};
|
|
229
312
|
}
|
|
230
313
|
|
|
231
314
|
//#endregion
|
|
232
315
|
//#region src/core/session.ts
|
|
233
316
|
/**
|
|
234
|
-
*
|
|
317
|
+
* Reads the current login status from `GET /api/session`.
|
|
318
|
+
*
|
|
319
|
+
* Works with the new `react-shopwave-connect/next` session route
|
|
320
|
+
* (`{ loggedIn }`) and with older routes that returned the raw session
|
|
321
|
+
* (`{ token: {...} }`) — in that case only a boolean is surfaced.
|
|
322
|
+
*/
|
|
323
|
+
async function fetchSession(options = {}) {
|
|
324
|
+
const doFetch = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
325
|
+
if (!doFetch) throw new Error("No fetch implementation available. Pass `options.fetch`.");
|
|
326
|
+
const response = await doFetch(`${options.baseUrl ?? ""}/api/session`, {
|
|
327
|
+
method: "GET",
|
|
328
|
+
headers: { Accept: "application/json" },
|
|
329
|
+
cache: "no-store",
|
|
330
|
+
credentials: "same-origin",
|
|
331
|
+
signal: options.signal
|
|
332
|
+
});
|
|
333
|
+
if (response.status === 401) return { loggedIn: false };
|
|
334
|
+
if (!response.ok) throw new Error(`Session request failed (${response.status})`);
|
|
335
|
+
const json = await response.json();
|
|
336
|
+
if (json && typeof json.loggedIn === "boolean") return {
|
|
337
|
+
loggedIn: json.loggedIn,
|
|
338
|
+
expiresAt: typeof json.expiresAt === "number" ? json.expiresAt : void 0
|
|
339
|
+
};
|
|
340
|
+
return { loggedIn: Boolean(json && json.token) };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Ends the current server session (no redirect).
|
|
235
344
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* built from server actions) is intentionally NOT included here because it is
|
|
239
|
-
* tied to `next/navigation` and `'use server'` actions — see the README.
|
|
345
|
+
* Prefer navigating to {@link logoutPath} when using
|
|
346
|
+
* `react-shopwave-connect/next`: it also signs the user out of the auth server.
|
|
240
347
|
*/
|
|
241
348
|
async function logout(options = {}) {
|
|
242
349
|
await apiDelete("/api/session?action=logout", options);
|
|
@@ -245,10 +352,12 @@ async function logout(options = {}) {
|
|
|
245
352
|
//#endregion
|
|
246
353
|
//#region src/core/entity.ts
|
|
247
354
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
355
|
+
* Low-level, untyped mutations: the caller supplies the route and payload.
|
|
356
|
+
*
|
|
357
|
+
* Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,
|
|
358
|
+
* …): they know each route, build the request envelope, check the response and
|
|
359
|
+
* return the saved entity with its id. Keep these for routes that have no typed
|
|
360
|
+
* function yet (e.g. `/api/merchant`).
|
|
252
361
|
*/
|
|
253
362
|
/**
|
|
254
363
|
* Deletes `/api/{resourcePath}/{entityId}`.
|
|
@@ -258,11 +367,15 @@ async function deleteEntity(resourcePath, entityId, options = {}) {
|
|
|
258
367
|
await apiDelete(`/api/${resourcePath}/${entityId}`, options);
|
|
259
368
|
}
|
|
260
369
|
/**
|
|
261
|
-
* Sends a create/update mutation
|
|
262
|
-
* (
|
|
370
|
+
* Sends a create/update mutation. Returns the response's `result` field when it
|
|
371
|
+
* has one (older app routes wrapped their answer that way), otherwise the whole
|
|
372
|
+
* parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.
|
|
263
373
|
*/
|
|
264
374
|
async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
|
|
265
|
-
|
|
375
|
+
const json = await apiSend(endpoint, method, payload, options);
|
|
376
|
+
assertNoApiErrors(json, 200);
|
|
377
|
+
if (json && typeof json === "object" && "result" in json) return json.result;
|
|
378
|
+
return json ?? void 0;
|
|
266
379
|
}
|
|
267
380
|
|
|
268
381
|
//#endregion
|
|
@@ -415,50 +528,127 @@ function toMessage(e) {
|
|
|
415
528
|
if (e instanceof Error) return e.message;
|
|
416
529
|
return String(e);
|
|
417
530
|
}
|
|
531
|
+
function toStatus(e) {
|
|
532
|
+
return e instanceof ShopwaveApiError ? e.status : null;
|
|
533
|
+
}
|
|
534
|
+
function queryReducer(state, action) {
|
|
535
|
+
switch (action.type) {
|
|
536
|
+
case "start":
|
|
537
|
+
if (action.key !== state.key) return {
|
|
538
|
+
data: null,
|
|
539
|
+
key: action.key,
|
|
540
|
+
fetching: true,
|
|
541
|
+
error: null,
|
|
542
|
+
errorStatus: null
|
|
543
|
+
};
|
|
544
|
+
return {
|
|
545
|
+
...state,
|
|
546
|
+
fetching: true,
|
|
547
|
+
error: null,
|
|
548
|
+
errorStatus: null
|
|
549
|
+
};
|
|
550
|
+
case "success":
|
|
551
|
+
if (action.key !== state.key) return state;
|
|
552
|
+
return {
|
|
553
|
+
data: action.data,
|
|
554
|
+
key: action.key,
|
|
555
|
+
fetching: false,
|
|
556
|
+
error: null,
|
|
557
|
+
errorStatus: null
|
|
558
|
+
};
|
|
559
|
+
case "failure":
|
|
560
|
+
if (action.key !== state.key) return state;
|
|
561
|
+
return {
|
|
562
|
+
...state,
|
|
563
|
+
fetching: false,
|
|
564
|
+
error: action.error,
|
|
565
|
+
errorStatus: action.errorStatus
|
|
566
|
+
};
|
|
567
|
+
case "idle": return state.fetching ? {
|
|
568
|
+
...state,
|
|
569
|
+
fetching: false
|
|
570
|
+
} : state;
|
|
571
|
+
default: return state;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Public view of the internal state for the params currently being rendered. */
|
|
575
|
+
function selectQueryView(state, key, enabled) {
|
|
576
|
+
if (state.key !== key) return {
|
|
577
|
+
data: null,
|
|
578
|
+
loading: enabled,
|
|
579
|
+
fetching: enabled,
|
|
580
|
+
error: null,
|
|
581
|
+
errorStatus: null
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
data: state.data,
|
|
585
|
+
loading: state.fetching && state.data === null,
|
|
586
|
+
fetching: state.fetching,
|
|
587
|
+
error: state.error,
|
|
588
|
+
errorStatus: state.errorStatus
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function depsKey(deps) {
|
|
592
|
+
try {
|
|
593
|
+
return JSON.stringify(deps);
|
|
594
|
+
} catch {
|
|
595
|
+
return String(deps);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
418
598
|
/**
|
|
419
599
|
* Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and
|
|
420
600
|
* whenever `deps` change, tracks loading/error/data, cancels in-flight requests
|
|
421
|
-
* on unmount or dep change, and exposes `refetch
|
|
601
|
+
* on unmount or dep change, and exposes `refetch` (which keeps the current data
|
|
602
|
+
* visible until the new data arrives).
|
|
422
603
|
*
|
|
423
604
|
* When `enabled` is false the fetch is skipped (used for hooks that need
|
|
424
605
|
* arguments before they can run, e.g. consumers-by-id).
|
|
425
606
|
*/
|
|
426
607
|
function useQuery(fetcher, deps, enabled = true) {
|
|
427
|
-
const
|
|
428
|
-
const [
|
|
429
|
-
|
|
608
|
+
const key = depsKey(deps);
|
|
609
|
+
const [state, dispatch] = (0, react.useReducer)(queryReducer, {
|
|
610
|
+
data: null,
|
|
611
|
+
key: enabled ? key : null,
|
|
612
|
+
fetching: enabled,
|
|
613
|
+
error: null,
|
|
614
|
+
errorStatus: null
|
|
615
|
+
});
|
|
430
616
|
const [tick, setTick] = (0, react.useState)(0);
|
|
431
617
|
const refetch = (0, react.useCallback)(() => setTick((t) => t + 1), []);
|
|
432
618
|
(0, react.useEffect)(() => {
|
|
433
619
|
if (!enabled) {
|
|
434
|
-
|
|
620
|
+
dispatch({ type: "idle" });
|
|
435
621
|
return;
|
|
436
622
|
}
|
|
437
623
|
const controller = new AbortController();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
624
|
+
dispatch({
|
|
625
|
+
type: "start",
|
|
626
|
+
key
|
|
627
|
+
});
|
|
442
628
|
fetcher(controller.signal).then((result) => {
|
|
443
|
-
if (
|
|
629
|
+
if (!controller.signal.aborted) dispatch({
|
|
630
|
+
type: "success",
|
|
631
|
+
key,
|
|
632
|
+
data: result
|
|
633
|
+
});
|
|
444
634
|
}).catch((e) => {
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
635
|
+
if (!controller.signal.aborted && e?.name !== "AbortError") dispatch({
|
|
636
|
+
type: "failure",
|
|
637
|
+
key,
|
|
638
|
+
error: toMessage(e),
|
|
639
|
+
errorStatus: toStatus(e)
|
|
640
|
+
});
|
|
448
641
|
});
|
|
449
642
|
return () => {
|
|
450
|
-
active = false;
|
|
451
643
|
controller.abort();
|
|
452
644
|
};
|
|
453
645
|
}, [
|
|
454
|
-
|
|
646
|
+
key,
|
|
455
647
|
tick,
|
|
456
648
|
enabled
|
|
457
649
|
]);
|
|
458
650
|
return {
|
|
459
|
-
|
|
460
|
-
loading,
|
|
461
|
-
error,
|
|
651
|
+
...selectQueryView(state, key, enabled),
|
|
462
652
|
refetch
|
|
463
653
|
};
|
|
464
654
|
}
|
|
@@ -469,16 +659,19 @@ function useMutation(action, deps) {
|
|
|
469
659
|
const [data, setData] = (0, react.useState)(null);
|
|
470
660
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
471
661
|
const [error, setError] = (0, react.useState)(null);
|
|
662
|
+
const [errorStatus, setErrorStatus] = (0, react.useState)(null);
|
|
472
663
|
return {
|
|
473
664
|
mutate: (0, react.useCallback)(async (...args) => {
|
|
474
665
|
setLoading(true);
|
|
475
666
|
setError(null);
|
|
667
|
+
setErrorStatus(null);
|
|
476
668
|
try {
|
|
477
669
|
const result = await action(...args);
|
|
478
670
|
setData(result);
|
|
479
671
|
return result;
|
|
480
672
|
} catch (e) {
|
|
481
673
|
setError(toMessage(e));
|
|
674
|
+
setErrorStatus(toStatus(e));
|
|
482
675
|
return null;
|
|
483
676
|
} finally {
|
|
484
677
|
setLoading(false);
|
|
@@ -486,7 +679,8 @@ function useMutation(action, deps) {
|
|
|
486
679
|
}, deps),
|
|
487
680
|
data,
|
|
488
681
|
loading,
|
|
489
|
-
error
|
|
682
|
+
error,
|
|
683
|
+
errorStatus
|
|
490
684
|
};
|
|
491
685
|
}
|
|
492
686
|
|
|
@@ -668,6 +862,23 @@ function useLogout(options) {
|
|
|
668
862
|
return useMutation(() => logout(options), [options?.baseUrl, options?.token]);
|
|
669
863
|
}
|
|
670
864
|
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/hooks/useSession.ts
|
|
867
|
+
/**
|
|
868
|
+
* Current login status (`{ loggedIn, expiresAt }`) from `GET /api/session`.
|
|
869
|
+
* Tokens are never exposed to the browser.
|
|
870
|
+
*
|
|
871
|
+
* @example
|
|
872
|
+
* const { data: session, loading } = useSession();
|
|
873
|
+
* if (!loading && !session?.loggedIn) window.location.href = loginPath(location.pathname);
|
|
874
|
+
*/
|
|
875
|
+
function useSession(options) {
|
|
876
|
+
return useQuery((signal) => fetchSession({
|
|
877
|
+
...options,
|
|
878
|
+
signal
|
|
879
|
+
}), [options?.baseUrl]);
|
|
880
|
+
}
|
|
881
|
+
|
|
671
882
|
//#endregion
|
|
672
883
|
//#region src/hooks/useBasketReport.ts
|
|
673
884
|
/**
|
|
@@ -781,6 +992,7 @@ exports.useLogout = useLogout;
|
|
|
781
992
|
exports.useProduct = useProduct;
|
|
782
993
|
exports.usePromotion = usePromotion;
|
|
783
994
|
exports.useReport = useReport;
|
|
995
|
+
exports.useSession = useSession;
|
|
784
996
|
exports.useStore = useStore;
|
|
785
997
|
exports.useSubmit = useSubmit;
|
|
786
998
|
//# sourceMappingURL=index.cjs.map
|