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.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,9 +307,8 @@ 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
|
|
@@ -269,10 +352,12 @@ async function logout(options = {}) {
|
|
|
269
352
|
//#endregion
|
|
270
353
|
//#region src/core/entity.ts
|
|
271
354
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
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`).
|
|
276
361
|
*/
|
|
277
362
|
/**
|
|
278
363
|
* Deletes `/api/{resourcePath}/{entityId}`.
|
|
@@ -282,11 +367,15 @@ async function deleteEntity(resourcePath, entityId, options = {}) {
|
|
|
282
367
|
await apiDelete(`/api/${resourcePath}/${entityId}`, options);
|
|
283
368
|
}
|
|
284
369
|
/**
|
|
285
|
-
* Sends a create/update mutation
|
|
286
|
-
* (
|
|
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.
|
|
287
373
|
*/
|
|
288
374
|
async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
|
|
289
|
-
|
|
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;
|
|
290
379
|
}
|
|
291
380
|
|
|
292
381
|
//#endregion
|
|
@@ -439,50 +528,127 @@ function toMessage(e) {
|
|
|
439
528
|
if (e instanceof Error) return e.message;
|
|
440
529
|
return String(e);
|
|
441
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
|
+
}
|
|
442
598
|
/**
|
|
443
599
|
* Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and
|
|
444
600
|
* whenever `deps` change, tracks loading/error/data, cancels in-flight requests
|
|
445
|
-
* 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).
|
|
446
603
|
*
|
|
447
604
|
* When `enabled` is false the fetch is skipped (used for hooks that need
|
|
448
605
|
* arguments before they can run, e.g. consumers-by-id).
|
|
449
606
|
*/
|
|
450
607
|
function useQuery(fetcher, deps, enabled = true) {
|
|
451
|
-
const
|
|
452
|
-
const [
|
|
453
|
-
|
|
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
|
+
});
|
|
454
616
|
const [tick, setTick] = (0, react.useState)(0);
|
|
455
617
|
const refetch = (0, react.useCallback)(() => setTick((t) => t + 1), []);
|
|
456
618
|
(0, react.useEffect)(() => {
|
|
457
619
|
if (!enabled) {
|
|
458
|
-
|
|
620
|
+
dispatch({ type: "idle" });
|
|
459
621
|
return;
|
|
460
622
|
}
|
|
461
623
|
const controller = new AbortController();
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
624
|
+
dispatch({
|
|
625
|
+
type: "start",
|
|
626
|
+
key
|
|
627
|
+
});
|
|
466
628
|
fetcher(controller.signal).then((result) => {
|
|
467
|
-
if (
|
|
629
|
+
if (!controller.signal.aborted) dispatch({
|
|
630
|
+
type: "success",
|
|
631
|
+
key,
|
|
632
|
+
data: result
|
|
633
|
+
});
|
|
468
634
|
}).catch((e) => {
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
635
|
+
if (!controller.signal.aborted && e?.name !== "AbortError") dispatch({
|
|
636
|
+
type: "failure",
|
|
637
|
+
key,
|
|
638
|
+
error: toMessage(e),
|
|
639
|
+
errorStatus: toStatus(e)
|
|
640
|
+
});
|
|
472
641
|
});
|
|
473
642
|
return () => {
|
|
474
|
-
active = false;
|
|
475
643
|
controller.abort();
|
|
476
644
|
};
|
|
477
645
|
}, [
|
|
478
|
-
|
|
646
|
+
key,
|
|
479
647
|
tick,
|
|
480
648
|
enabled
|
|
481
649
|
]);
|
|
482
650
|
return {
|
|
483
|
-
|
|
484
|
-
loading,
|
|
485
|
-
error,
|
|
651
|
+
...selectQueryView(state, key, enabled),
|
|
486
652
|
refetch
|
|
487
653
|
};
|
|
488
654
|
}
|
|
@@ -493,16 +659,19 @@ function useMutation(action, deps) {
|
|
|
493
659
|
const [data, setData] = (0, react.useState)(null);
|
|
494
660
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
495
661
|
const [error, setError] = (0, react.useState)(null);
|
|
662
|
+
const [errorStatus, setErrorStatus] = (0, react.useState)(null);
|
|
496
663
|
return {
|
|
497
664
|
mutate: (0, react.useCallback)(async (...args) => {
|
|
498
665
|
setLoading(true);
|
|
499
666
|
setError(null);
|
|
667
|
+
setErrorStatus(null);
|
|
500
668
|
try {
|
|
501
669
|
const result = await action(...args);
|
|
502
670
|
setData(result);
|
|
503
671
|
return result;
|
|
504
672
|
} catch (e) {
|
|
505
673
|
setError(toMessage(e));
|
|
674
|
+
setErrorStatus(toStatus(e));
|
|
506
675
|
return null;
|
|
507
676
|
} finally {
|
|
508
677
|
setLoading(false);
|
|
@@ -510,7 +679,8 @@ function useMutation(action, deps) {
|
|
|
510
679
|
}, deps),
|
|
511
680
|
data,
|
|
512
681
|
loading,
|
|
513
|
-
error
|
|
682
|
+
error,
|
|
683
|
+
errorStatus
|
|
514
684
|
};
|
|
515
685
|
}
|
|
516
686
|
|