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.js
CHANGED
|
@@ -1,88 +1,179 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
|
2
2
|
|
|
3
|
+
//#region src/core/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by every `core` function when a request fails, either at the HTTP
|
|
6
|
+
* level (`status` is the HTTP status) or because the Shopwave response carried
|
|
7
|
+
* `api.message.errors` (then `status` is the HTTP status of that response,
|
|
8
|
+
* usually 200/201, and `errors` holds the API errors).
|
|
9
|
+
*
|
|
10
|
+
* Branch on `status` (e.g. `isUnauthorized` → send the user to log in) rather
|
|
11
|
+
* than parsing `message`.
|
|
12
|
+
*/
|
|
13
|
+
var ShopwaveApiError = class extends Error {
|
|
14
|
+
/** HTTP status of the response (0 when the request never got a response). */
|
|
15
|
+
status;
|
|
16
|
+
/** Shopwave API errors, when the response carried any. */
|
|
17
|
+
errors;
|
|
18
|
+
/** Parsed response body, when there was one. */
|
|
19
|
+
body;
|
|
20
|
+
constructor(status, message, details = {}) {
|
|
21
|
+
super(message, details.cause !== void 0 ? { cause: details.cause } : void 0);
|
|
22
|
+
this.name = "ShopwaveApiError";
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.errors = details.errors ?? null;
|
|
25
|
+
this.body = details.body;
|
|
26
|
+
}
|
|
27
|
+
/** 401, or the Shopwave "token expired / invalid" error 908. */
|
|
28
|
+
get isUnauthorized() {
|
|
29
|
+
if (this.status === 401) return true;
|
|
30
|
+
return !!this.errors && Object.entries(this.errors).some(([k, v]) => k === "908" || Number(v?.id) === 908);
|
|
31
|
+
}
|
|
32
|
+
get isNotFound() {
|
|
33
|
+
return this.status === 404;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/** The `api.message.errors` (or `api.message.error`) map of a Shopwave response, or `null` when there are none. */
|
|
37
|
+
function getApiErrorMap(body) {
|
|
38
|
+
const message = body?.api?.message;
|
|
39
|
+
const errors = message?.errors ?? message?.error;
|
|
40
|
+
if (errors && typeof errors === "object" && Object.keys(errors).length > 0) return errors;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function describeErrors(errors) {
|
|
44
|
+
return Object.entries(errors).map(([code, e]) => {
|
|
45
|
+
const text = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
46
|
+
return text ? `${code} ${text}` : code;
|
|
47
|
+
}).join("; ");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds a {@link ShopwaveApiError} for a response. Understands the Shopwave
|
|
51
|
+
* envelope (`api.message.errors`) and the `{ error, message }` bodies returned
|
|
52
|
+
* by the `react-shopwave-connect/next` routes.
|
|
53
|
+
*/
|
|
54
|
+
function toShopwaveApiError(status, body, fallback) {
|
|
55
|
+
const errors = getApiErrorMap(body);
|
|
56
|
+
let detail = errors ? describeErrors(errors) : "";
|
|
57
|
+
if (!detail && body && typeof body === "object") {
|
|
58
|
+
const b = body;
|
|
59
|
+
if (typeof b.message === "string") detail = b.message;
|
|
60
|
+
else if (typeof b.error === "string") detail = b.error;
|
|
61
|
+
}
|
|
62
|
+
if (!detail) detail = fallback ?? (status >= 400 ? "request failed" : "API returned errors");
|
|
63
|
+
return new ShopwaveApiError(status, `Shopwave API error (${status}): ${detail}`, {
|
|
64
|
+
errors,
|
|
65
|
+
body
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Throws a {@link ShopwaveApiError} when the response body carries API errors. */
|
|
69
|
+
function assertNoApiErrors(body, status) {
|
|
70
|
+
if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
3
74
|
//#region src/core/request.ts
|
|
4
75
|
function resolveFetch(options) {
|
|
5
76
|
const f = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
6
77
|
if (!f) throw new Error("No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.");
|
|
7
78
|
return f;
|
|
8
79
|
}
|
|
9
|
-
|
|
80
|
+
/** Options with `token` filled in from a legacy per-call `params.token`, unless `options.token` is set. */
|
|
81
|
+
function withToken(options, token) {
|
|
82
|
+
return token && !options.token ? {
|
|
83
|
+
...options,
|
|
84
|
+
token
|
|
85
|
+
} : options;
|
|
86
|
+
}
|
|
87
|
+
/** `Authorization` header value for a token (`OAuth <token>` unless it already has a scheme). */
|
|
88
|
+
function authorizationFor(token) {
|
|
89
|
+
const t = token.trim();
|
|
90
|
+
return /^(oauth|bearer)\s+\S/i.test(t) ? t : `OAuth ${t}`;
|
|
91
|
+
}
|
|
92
|
+
/** Headers shared by every request: Accept + the token, when one is given. */
|
|
93
|
+
function baseHeaders(options) {
|
|
94
|
+
const headers = { Accept: "application/json" };
|
|
95
|
+
if (options.token) headers.Authorization = authorizationFor(options.token);
|
|
96
|
+
return headers;
|
|
97
|
+
}
|
|
98
|
+
function buildExtras(extras) {
|
|
10
99
|
const merged = {
|
|
11
100
|
"Content-Type": "application/json",
|
|
12
101
|
...extras
|
|
13
102
|
};
|
|
14
|
-
|
|
103
|
+
delete merged.token;
|
|
15
104
|
return merged;
|
|
16
105
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
106
|
+
/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
|
|
107
|
+
async function readBody(response) {
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
if (!text) return null;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(text);
|
|
112
|
+
} catch {
|
|
113
|
+
return { raw: text };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function send(path, init, options) {
|
|
117
|
+
const doFetch = resolveFetch(options);
|
|
118
|
+
const url = (options.baseUrl ?? "") + path;
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await doFetch(url, {
|
|
122
|
+
...init,
|
|
123
|
+
signal: options.signal
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error?.name === "AbortError") throw error;
|
|
127
|
+
throw new ShopwaveApiError(0, `Shopwave API request failed: ${error?.message ?? error}`, { cause: error });
|
|
128
|
+
}
|
|
129
|
+
const body = await readBody(response);
|
|
130
|
+
if (!response.ok) throw toShopwaveApiError(response.status, body, response.statusText || void 0);
|
|
131
|
+
return {
|
|
132
|
+
status: response.status,
|
|
133
|
+
body
|
|
134
|
+
};
|
|
25
135
|
}
|
|
26
136
|
/**
|
|
27
|
-
* GET helper. Sends request metadata via the `extras` header
|
|
28
|
-
*
|
|
137
|
+
* GET helper. Sends request metadata via the `extras` header, parses JSON and
|
|
138
|
+
* returns the payload. Throws {@link ShopwaveApiError} for non-2xx responses.
|
|
29
139
|
*/
|
|
30
140
|
async function apiGet(path, extras, options = {}) {
|
|
31
|
-
const
|
|
32
|
-
const url = (options.baseUrl ?? "") + path;
|
|
33
|
-
const headers = { extras: JSON.stringify(buildExtras(extras, options)) };
|
|
34
|
-
console.log("[API GET]", {
|
|
35
|
-
url,
|
|
36
|
-
headers
|
|
37
|
-
});
|
|
38
|
-
const response = await doFetch(url, {
|
|
141
|
+
const { body } = await send(path, {
|
|
39
142
|
method: "GET",
|
|
40
|
-
headers
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
143
|
+
headers: {
|
|
144
|
+
...baseHeaders(options),
|
|
145
|
+
extras: JSON.stringify(buildExtras(extras))
|
|
146
|
+
}
|
|
147
|
+
}, options);
|
|
148
|
+
return body;
|
|
45
149
|
}
|
|
46
150
|
/**
|
|
47
|
-
* JSON body helper for POST/PUT mutations.
|
|
151
|
+
* JSON body helper for POST/PUT mutations. Returns the parsed body (`null` for
|
|
152
|
+
* an empty one). Throws {@link ShopwaveApiError} for non-2xx responses.
|
|
48
153
|
*/
|
|
49
154
|
async function apiSend(path, method, payload, options = {}) {
|
|
50
|
-
const
|
|
51
|
-
const url = (options.baseUrl ?? "") + path;
|
|
52
|
-
const headers = { "Content-Type": "application/json" };
|
|
53
|
-
if (options.token != null) headers.token = options.token;
|
|
54
|
-
console.log(`[API ${method}]`, {
|
|
55
|
-
url,
|
|
56
|
-
headers,
|
|
57
|
-
payload
|
|
58
|
-
});
|
|
59
|
-
const response = await doFetch(url, {
|
|
155
|
+
const { body } = await send(path, {
|
|
60
156
|
method,
|
|
61
|
-
headers
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
157
|
+
headers: {
|
|
158
|
+
...baseHeaders(options),
|
|
159
|
+
"Content-Type": "application/json"
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(payload)
|
|
162
|
+
}, options);
|
|
163
|
+
return body;
|
|
67
164
|
}
|
|
68
165
|
/**
|
|
69
|
-
* DELETE helper.
|
|
166
|
+
* DELETE helper. Shopwave answers deletes with 205 and an empty body; if a body
|
|
167
|
+
* does come back and carries API errors, they are thrown. Returns the parsed
|
|
168
|
+
* body (`null` when empty).
|
|
70
169
|
*/
|
|
71
170
|
async function apiDelete(path, options = {}) {
|
|
72
|
-
const
|
|
73
|
-
const url = (options.baseUrl ?? "") + path;
|
|
74
|
-
const headers = {};
|
|
75
|
-
if (options.token != null) headers.token = options.token;
|
|
76
|
-
console.log("[API DELETE]", {
|
|
77
|
-
url,
|
|
78
|
-
headers
|
|
79
|
-
});
|
|
80
|
-
const response = await doFetch(url, {
|
|
171
|
+
const { status, body } = await send(path, {
|
|
81
172
|
method: "DELETE",
|
|
82
|
-
headers
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
173
|
+
headers: baseHeaders(options)
|
|
174
|
+
}, options);
|
|
175
|
+
if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
|
|
176
|
+
return body;
|
|
86
177
|
}
|
|
87
178
|
|
|
88
179
|
//#endregion
|
|
@@ -94,11 +185,9 @@ async function apiDelete(path, options = {}) {
|
|
|
94
185
|
async function fetchCategories(params = {}, options = {}) {
|
|
95
186
|
const extras = { deleted: params.deleted ?? false };
|
|
96
187
|
if (params.categoryIds) extras.categoryIds = params.categoryIds;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (error) throw new Error(error);
|
|
101
|
-
return Object.values(json.categories ?? {});
|
|
188
|
+
const json = await apiGet("/api/categories", extras, withToken(options, params.token));
|
|
189
|
+
assertNoApiErrors(json, 200);
|
|
190
|
+
return Object.values(json?.categories ?? {});
|
|
102
191
|
}
|
|
103
192
|
|
|
104
193
|
//#endregion
|
|
@@ -110,9 +199,8 @@ async function fetchCategories(params = {}, options = {}) {
|
|
|
110
199
|
async function fetchConsumers(consumerIds, options = {}) {
|
|
111
200
|
if (!consumerIds || consumerIds.length === 0) return [];
|
|
112
201
|
const json = await apiGet("/api/consumer", { ids: consumerIds.join(",") }, options);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return Object.values(json.consumers ?? {});
|
|
202
|
+
assertNoApiErrors(json, 200);
|
|
203
|
+
return Object.values(json?.consumers ?? {});
|
|
116
204
|
}
|
|
117
205
|
|
|
118
206
|
//#endregion
|
|
@@ -123,10 +211,9 @@ async function fetchConsumers(consumerIds, options = {}) {
|
|
|
123
211
|
async function fetchEmployees(params = {}, options = {}) {
|
|
124
212
|
const extras = { deleted: params.deleted ?? false };
|
|
125
213
|
if (params.employeeIds) extras.employeeIds = params.employeeIds;
|
|
126
|
-
const json = await apiGet("/api/employees", extras, options);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return Object.values(json.employees ?? {});
|
|
214
|
+
const json = await apiGet("/api/employees", extras, withToken(options, params.token));
|
|
215
|
+
assertNoApiErrors(json, 200);
|
|
216
|
+
return Object.values(json?.employees ?? {});
|
|
130
217
|
}
|
|
131
218
|
|
|
132
219
|
//#endregion
|
|
@@ -138,11 +225,9 @@ async function fetchProducts(params = {}, options = {}) {
|
|
|
138
225
|
const extras = { deleted: params.deleted ?? false };
|
|
139
226
|
if (params.productIds) extras.productIds = params.productIds;
|
|
140
227
|
if (params.storeId) extras.storeId = params.storeId;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (error) throw new Error(error);
|
|
145
|
-
return Object.values(json.products ?? {});
|
|
228
|
+
const json = await apiGet("/api/products", extras, withToken(options, params.token));
|
|
229
|
+
assertNoApiErrors(json, 200);
|
|
230
|
+
return Object.values(json?.products ?? {});
|
|
146
231
|
}
|
|
147
232
|
/**
|
|
148
233
|
* Fetches products by id and returns them keyed by id, transparently batching
|
|
@@ -171,11 +256,9 @@ async function fetchPromotions(params = {}, options = {}) {
|
|
|
171
256
|
if (params.imminent !== void 0) extras.imminent = params.imminent;
|
|
172
257
|
if (params.active !== void 0) extras.active = params.active;
|
|
173
258
|
if (params.expired !== void 0) extras.expired = params.expired;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (error) throw new Error(error);
|
|
178
|
-
return Object.values(json.promotions ?? {});
|
|
259
|
+
const json = await apiGet("/api/promotions", extras, withToken(options, params.token));
|
|
260
|
+
assertNoApiErrors(json, 200);
|
|
261
|
+
return Object.values(json?.promotions ?? {});
|
|
179
262
|
}
|
|
180
263
|
|
|
181
264
|
//#endregion
|
|
@@ -184,10 +267,11 @@ async function fetchPromotions(params = {}, options = {}) {
|
|
|
184
267
|
* Fetches stores. Returns the flattened `Store[]`.
|
|
185
268
|
*/
|
|
186
269
|
async function fetchStores(params = {}, options = {}) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
270
|
+
const extras = { deleted: params.deleted ?? false };
|
|
271
|
+
if (params.storeIds) extras.storeIds = params.storeIds;
|
|
272
|
+
const json = await apiGet("/api/stores", extras, withToken(options, params.token));
|
|
273
|
+
assertNoApiErrors(json, 200);
|
|
274
|
+
return Object.values(json?.stores ?? {});
|
|
191
275
|
}
|
|
192
276
|
|
|
193
277
|
//#endregion
|
|
@@ -199,20 +283,43 @@ async function fetchStores(params = {}, options = {}) {
|
|
|
199
283
|
*/
|
|
200
284
|
async function fetchReport(query, options = {}) {
|
|
201
285
|
const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return json.reports ?? {};
|
|
286
|
+
assertNoApiErrors(json, 200);
|
|
287
|
+
return json?.reports ?? {};
|
|
205
288
|
}
|
|
206
289
|
|
|
207
290
|
//#endregion
|
|
208
291
|
//#region src/core/session.ts
|
|
209
292
|
/**
|
|
210
|
-
*
|
|
293
|
+
* Reads the current login status from `GET /api/session`.
|
|
294
|
+
*
|
|
295
|
+
* Works with the new `react-shopwave-connect/next` session route
|
|
296
|
+
* (`{ loggedIn }`) and with older routes that returned the raw session
|
|
297
|
+
* (`{ token: {...} }`) — in that case only a boolean is surfaced.
|
|
298
|
+
*/
|
|
299
|
+
async function fetchSession(options = {}) {
|
|
300
|
+
const doFetch = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
301
|
+
if (!doFetch) throw new Error("No fetch implementation available. Pass `options.fetch`.");
|
|
302
|
+
const response = await doFetch(`${options.baseUrl ?? ""}/api/session`, {
|
|
303
|
+
method: "GET",
|
|
304
|
+
headers: { Accept: "application/json" },
|
|
305
|
+
cache: "no-store",
|
|
306
|
+
credentials: "same-origin",
|
|
307
|
+
signal: options.signal
|
|
308
|
+
});
|
|
309
|
+
if (response.status === 401) return { loggedIn: false };
|
|
310
|
+
if (!response.ok) throw new Error(`Session request failed (${response.status})`);
|
|
311
|
+
const json = await response.json();
|
|
312
|
+
if (json && typeof json.loggedIn === "boolean") return {
|
|
313
|
+
loggedIn: json.loggedIn,
|
|
314
|
+
expiresAt: typeof json.expiresAt === "number" ? json.expiresAt : void 0
|
|
315
|
+
};
|
|
316
|
+
return { loggedIn: Boolean(json && json.token) };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Ends the current server session (no redirect).
|
|
211
320
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* built from server actions) is intentionally NOT included here because it is
|
|
215
|
-
* tied to `next/navigation` and `'use server'` actions — see the README.
|
|
321
|
+
* Prefer navigating to {@link logoutPath} when using
|
|
322
|
+
* `react-shopwave-connect/next`: it also signs the user out of the auth server.
|
|
216
323
|
*/
|
|
217
324
|
async function logout(options = {}) {
|
|
218
325
|
await apiDelete("/api/session?action=logout", options);
|
|
@@ -221,10 +328,12 @@ async function logout(options = {}) {
|
|
|
221
328
|
//#endregion
|
|
222
329
|
//#region src/core/entity.ts
|
|
223
330
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
331
|
+
* Low-level, untyped mutations: the caller supplies the route and payload.
|
|
332
|
+
*
|
|
333
|
+
* Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,
|
|
334
|
+
* …): they know each route, build the request envelope, check the response and
|
|
335
|
+
* return the saved entity with its id. Keep these for routes that have no typed
|
|
336
|
+
* function yet (e.g. `/api/merchant`).
|
|
228
337
|
*/
|
|
229
338
|
/**
|
|
230
339
|
* Deletes `/api/{resourcePath}/{entityId}`.
|
|
@@ -234,11 +343,15 @@ async function deleteEntity(resourcePath, entityId, options = {}) {
|
|
|
234
343
|
await apiDelete(`/api/${resourcePath}/${entityId}`, options);
|
|
235
344
|
}
|
|
236
345
|
/**
|
|
237
|
-
* Sends a create/update mutation
|
|
238
|
-
* (
|
|
346
|
+
* Sends a create/update mutation. Returns the response's `result` field when it
|
|
347
|
+
* has one (older app routes wrapped their answer that way), otherwise the whole
|
|
348
|
+
* parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.
|
|
239
349
|
*/
|
|
240
350
|
async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
|
|
241
|
-
|
|
351
|
+
const json = await apiSend(endpoint, method, payload, options);
|
|
352
|
+
assertNoApiErrors(json, 200);
|
|
353
|
+
if (json && typeof json === "object" && "result" in json) return json.result;
|
|
354
|
+
return json ?? void 0;
|
|
242
355
|
}
|
|
243
356
|
|
|
244
357
|
//#endregion
|
|
@@ -391,50 +504,127 @@ function toMessage(e) {
|
|
|
391
504
|
if (e instanceof Error) return e.message;
|
|
392
505
|
return String(e);
|
|
393
506
|
}
|
|
507
|
+
function toStatus(e) {
|
|
508
|
+
return e instanceof ShopwaveApiError ? e.status : null;
|
|
509
|
+
}
|
|
510
|
+
function queryReducer(state, action) {
|
|
511
|
+
switch (action.type) {
|
|
512
|
+
case "start":
|
|
513
|
+
if (action.key !== state.key) return {
|
|
514
|
+
data: null,
|
|
515
|
+
key: action.key,
|
|
516
|
+
fetching: true,
|
|
517
|
+
error: null,
|
|
518
|
+
errorStatus: null
|
|
519
|
+
};
|
|
520
|
+
return {
|
|
521
|
+
...state,
|
|
522
|
+
fetching: true,
|
|
523
|
+
error: null,
|
|
524
|
+
errorStatus: null
|
|
525
|
+
};
|
|
526
|
+
case "success":
|
|
527
|
+
if (action.key !== state.key) return state;
|
|
528
|
+
return {
|
|
529
|
+
data: action.data,
|
|
530
|
+
key: action.key,
|
|
531
|
+
fetching: false,
|
|
532
|
+
error: null,
|
|
533
|
+
errorStatus: null
|
|
534
|
+
};
|
|
535
|
+
case "failure":
|
|
536
|
+
if (action.key !== state.key) return state;
|
|
537
|
+
return {
|
|
538
|
+
...state,
|
|
539
|
+
fetching: false,
|
|
540
|
+
error: action.error,
|
|
541
|
+
errorStatus: action.errorStatus
|
|
542
|
+
};
|
|
543
|
+
case "idle": return state.fetching ? {
|
|
544
|
+
...state,
|
|
545
|
+
fetching: false
|
|
546
|
+
} : state;
|
|
547
|
+
default: return state;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/** Public view of the internal state for the params currently being rendered. */
|
|
551
|
+
function selectQueryView(state, key, enabled) {
|
|
552
|
+
if (state.key !== key) return {
|
|
553
|
+
data: null,
|
|
554
|
+
loading: enabled,
|
|
555
|
+
fetching: enabled,
|
|
556
|
+
error: null,
|
|
557
|
+
errorStatus: null
|
|
558
|
+
};
|
|
559
|
+
return {
|
|
560
|
+
data: state.data,
|
|
561
|
+
loading: state.fetching && state.data === null,
|
|
562
|
+
fetching: state.fetching,
|
|
563
|
+
error: state.error,
|
|
564
|
+
errorStatus: state.errorStatus
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function depsKey(deps) {
|
|
568
|
+
try {
|
|
569
|
+
return JSON.stringify(deps);
|
|
570
|
+
} catch {
|
|
571
|
+
return String(deps);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
394
574
|
/**
|
|
395
575
|
* Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and
|
|
396
576
|
* whenever `deps` change, tracks loading/error/data, cancels in-flight requests
|
|
397
|
-
* on unmount or dep change, and exposes `refetch
|
|
577
|
+
* on unmount or dep change, and exposes `refetch` (which keeps the current data
|
|
578
|
+
* visible until the new data arrives).
|
|
398
579
|
*
|
|
399
580
|
* When `enabled` is false the fetch is skipped (used for hooks that need
|
|
400
581
|
* arguments before they can run, e.g. consumers-by-id).
|
|
401
582
|
*/
|
|
402
583
|
function useQuery(fetcher, deps, enabled = true) {
|
|
403
|
-
const
|
|
404
|
-
const [
|
|
405
|
-
|
|
584
|
+
const key = depsKey(deps);
|
|
585
|
+
const [state, dispatch] = useReducer(queryReducer, {
|
|
586
|
+
data: null,
|
|
587
|
+
key: enabled ? key : null,
|
|
588
|
+
fetching: enabled,
|
|
589
|
+
error: null,
|
|
590
|
+
errorStatus: null
|
|
591
|
+
});
|
|
406
592
|
const [tick, setTick] = useState(0);
|
|
407
593
|
const refetch = useCallback(() => setTick((t) => t + 1), []);
|
|
408
594
|
useEffect(() => {
|
|
409
595
|
if (!enabled) {
|
|
410
|
-
|
|
596
|
+
dispatch({ type: "idle" });
|
|
411
597
|
return;
|
|
412
598
|
}
|
|
413
599
|
const controller = new AbortController();
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
600
|
+
dispatch({
|
|
601
|
+
type: "start",
|
|
602
|
+
key
|
|
603
|
+
});
|
|
418
604
|
fetcher(controller.signal).then((result) => {
|
|
419
|
-
if (
|
|
605
|
+
if (!controller.signal.aborted) dispatch({
|
|
606
|
+
type: "success",
|
|
607
|
+
key,
|
|
608
|
+
data: result
|
|
609
|
+
});
|
|
420
610
|
}).catch((e) => {
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
611
|
+
if (!controller.signal.aborted && e?.name !== "AbortError") dispatch({
|
|
612
|
+
type: "failure",
|
|
613
|
+
key,
|
|
614
|
+
error: toMessage(e),
|
|
615
|
+
errorStatus: toStatus(e)
|
|
616
|
+
});
|
|
424
617
|
});
|
|
425
618
|
return () => {
|
|
426
|
-
active = false;
|
|
427
619
|
controller.abort();
|
|
428
620
|
};
|
|
429
621
|
}, [
|
|
430
|
-
|
|
622
|
+
key,
|
|
431
623
|
tick,
|
|
432
624
|
enabled
|
|
433
625
|
]);
|
|
434
626
|
return {
|
|
435
|
-
|
|
436
|
-
loading,
|
|
437
|
-
error,
|
|
627
|
+
...selectQueryView(state, key, enabled),
|
|
438
628
|
refetch
|
|
439
629
|
};
|
|
440
630
|
}
|
|
@@ -445,16 +635,19 @@ function useMutation(action, deps) {
|
|
|
445
635
|
const [data, setData] = useState(null);
|
|
446
636
|
const [loading, setLoading] = useState(false);
|
|
447
637
|
const [error, setError] = useState(null);
|
|
638
|
+
const [errorStatus, setErrorStatus] = useState(null);
|
|
448
639
|
return {
|
|
449
640
|
mutate: useCallback(async (...args) => {
|
|
450
641
|
setLoading(true);
|
|
451
642
|
setError(null);
|
|
643
|
+
setErrorStatus(null);
|
|
452
644
|
try {
|
|
453
645
|
const result = await action(...args);
|
|
454
646
|
setData(result);
|
|
455
647
|
return result;
|
|
456
648
|
} catch (e) {
|
|
457
649
|
setError(toMessage(e));
|
|
650
|
+
setErrorStatus(toStatus(e));
|
|
458
651
|
return null;
|
|
459
652
|
} finally {
|
|
460
653
|
setLoading(false);
|
|
@@ -462,7 +655,8 @@ function useMutation(action, deps) {
|
|
|
462
655
|
}, deps),
|
|
463
656
|
data,
|
|
464
657
|
loading,
|
|
465
|
-
error
|
|
658
|
+
error,
|
|
659
|
+
errorStatus
|
|
466
660
|
};
|
|
467
661
|
}
|
|
468
662
|
|
|
@@ -644,6 +838,23 @@ function useLogout(options) {
|
|
|
644
838
|
return useMutation(() => logout(options), [options?.baseUrl, options?.token]);
|
|
645
839
|
}
|
|
646
840
|
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/hooks/useSession.ts
|
|
843
|
+
/**
|
|
844
|
+
* Current login status (`{ loggedIn, expiresAt }`) from `GET /api/session`.
|
|
845
|
+
* Tokens are never exposed to the browser.
|
|
846
|
+
*
|
|
847
|
+
* @example
|
|
848
|
+
* const { data: session, loading } = useSession();
|
|
849
|
+
* if (!loading && !session?.loggedIn) window.location.href = loginPath(location.pathname);
|
|
850
|
+
*/
|
|
851
|
+
function useSession(options) {
|
|
852
|
+
return useQuery((signal) => fetchSession({
|
|
853
|
+
...options,
|
|
854
|
+
signal
|
|
855
|
+
}), [options?.baseUrl]);
|
|
856
|
+
}
|
|
857
|
+
|
|
647
858
|
//#endregion
|
|
648
859
|
//#region src/hooks/useBasketReport.ts
|
|
649
860
|
/**
|
|
@@ -748,5 +959,5 @@ function useBasketReport(options) {
|
|
|
748
959
|
}
|
|
749
960
|
|
|
750
961
|
//#endregion
|
|
751
|
-
export { useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useProduct, usePromotion, useReport, useStore, useSubmit };
|
|
962
|
+
export { useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useProduct, usePromotion, useReport, useSession, useStore, useSubmit };
|
|
752
963
|
//# sourceMappingURL=index.js.map
|