react-shopwave-connect 0.2.0 → 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 +145 -22
- package/dist/core/index.cjs +392 -88
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +214 -23
- package/dist/core/index.d.ts +214 -23
- package/dist/core/index.js +365 -89
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +281 -111
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +34 -11
- package/dist/hooks/index.d.ts +34 -11
- package/dist/hooks/index.js +282 -112
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +492 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -24
- package/dist/index.d.ts +227 -24
- package/dist/index.js +466 -109
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +436 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +169 -1
- package/dist/next/index.d.ts +169 -1
- package/dist/next/index.js +434 -3
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +411 -1
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +170 -1
- package/dist/server/index.d.ts +170 -1
- package/dist/server/index.js +404 -2
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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,9 +283,8 @@ 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
|
|
@@ -245,10 +328,12 @@ async function logout(options = {}) {
|
|
|
245
328
|
//#endregion
|
|
246
329
|
//#region src/core/entity.ts
|
|
247
330
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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`).
|
|
252
337
|
*/
|
|
253
338
|
/**
|
|
254
339
|
* Deletes `/api/{resourcePath}/{entityId}`.
|
|
@@ -258,11 +343,15 @@ async function deleteEntity(resourcePath, entityId, options = {}) {
|
|
|
258
343
|
await apiDelete(`/api/${resourcePath}/${entityId}`, options);
|
|
259
344
|
}
|
|
260
345
|
/**
|
|
261
|
-
* Sends a create/update mutation
|
|
262
|
-
* (
|
|
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.
|
|
263
349
|
*/
|
|
264
350
|
async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
|
|
265
|
-
|
|
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;
|
|
266
355
|
}
|
|
267
356
|
|
|
268
357
|
//#endregion
|
|
@@ -415,50 +504,127 @@ function toMessage(e) {
|
|
|
415
504
|
if (e instanceof Error) return e.message;
|
|
416
505
|
return String(e);
|
|
417
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
|
+
}
|
|
418
574
|
/**
|
|
419
575
|
* Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and
|
|
420
576
|
* whenever `deps` change, tracks loading/error/data, cancels in-flight requests
|
|
421
|
-
* 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).
|
|
422
579
|
*
|
|
423
580
|
* When `enabled` is false the fetch is skipped (used for hooks that need
|
|
424
581
|
* arguments before they can run, e.g. consumers-by-id).
|
|
425
582
|
*/
|
|
426
583
|
function useQuery(fetcher, deps, enabled = true) {
|
|
427
|
-
const
|
|
428
|
-
const [
|
|
429
|
-
|
|
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
|
+
});
|
|
430
592
|
const [tick, setTick] = useState(0);
|
|
431
593
|
const refetch = useCallback(() => setTick((t) => t + 1), []);
|
|
432
594
|
useEffect(() => {
|
|
433
595
|
if (!enabled) {
|
|
434
|
-
|
|
596
|
+
dispatch({ type: "idle" });
|
|
435
597
|
return;
|
|
436
598
|
}
|
|
437
599
|
const controller = new AbortController();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
600
|
+
dispatch({
|
|
601
|
+
type: "start",
|
|
602
|
+
key
|
|
603
|
+
});
|
|
442
604
|
fetcher(controller.signal).then((result) => {
|
|
443
|
-
if (
|
|
605
|
+
if (!controller.signal.aborted) dispatch({
|
|
606
|
+
type: "success",
|
|
607
|
+
key,
|
|
608
|
+
data: result
|
|
609
|
+
});
|
|
444
610
|
}).catch((e) => {
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
611
|
+
if (!controller.signal.aborted && e?.name !== "AbortError") dispatch({
|
|
612
|
+
type: "failure",
|
|
613
|
+
key,
|
|
614
|
+
error: toMessage(e),
|
|
615
|
+
errorStatus: toStatus(e)
|
|
616
|
+
});
|
|
448
617
|
});
|
|
449
618
|
return () => {
|
|
450
|
-
active = false;
|
|
451
619
|
controller.abort();
|
|
452
620
|
};
|
|
453
621
|
}, [
|
|
454
|
-
|
|
622
|
+
key,
|
|
455
623
|
tick,
|
|
456
624
|
enabled
|
|
457
625
|
]);
|
|
458
626
|
return {
|
|
459
|
-
|
|
460
|
-
loading,
|
|
461
|
-
error,
|
|
627
|
+
...selectQueryView(state, key, enabled),
|
|
462
628
|
refetch
|
|
463
629
|
};
|
|
464
630
|
}
|
|
@@ -469,16 +635,19 @@ function useMutation(action, deps) {
|
|
|
469
635
|
const [data, setData] = useState(null);
|
|
470
636
|
const [loading, setLoading] = useState(false);
|
|
471
637
|
const [error, setError] = useState(null);
|
|
638
|
+
const [errorStatus, setErrorStatus] = useState(null);
|
|
472
639
|
return {
|
|
473
640
|
mutate: useCallback(async (...args) => {
|
|
474
641
|
setLoading(true);
|
|
475
642
|
setError(null);
|
|
643
|
+
setErrorStatus(null);
|
|
476
644
|
try {
|
|
477
645
|
const result = await action(...args);
|
|
478
646
|
setData(result);
|
|
479
647
|
return result;
|
|
480
648
|
} catch (e) {
|
|
481
649
|
setError(toMessage(e));
|
|
650
|
+
setErrorStatus(toStatus(e));
|
|
482
651
|
return null;
|
|
483
652
|
} finally {
|
|
484
653
|
setLoading(false);
|
|
@@ -486,7 +655,8 @@ function useMutation(action, deps) {
|
|
|
486
655
|
}, deps),
|
|
487
656
|
data,
|
|
488
657
|
loading,
|
|
489
|
-
error
|
|
658
|
+
error,
|
|
659
|
+
errorStatus
|
|
490
660
|
};
|
|
491
661
|
}
|
|
492
662
|
|