@thepassle/app-tools 0.9.12 → 0.9.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thepassle/app-tools",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "scripts": {
package/router/index.js CHANGED
@@ -182,23 +182,29 @@ export class Router extends EventTarget {
182
182
  /** @type {Plugin[]} */
183
183
  let plugins = this._collectPlugins(route);
184
184
 
185
- for (const plugin of plugins) {
186
- try {
187
- const result = await plugin?.shouldNavigate?.(this.context);
188
- if (result) {
189
- const condition = await result.condition();
190
- if (!condition) {
191
- url = new URL(result.redirect, this.baseUrl);
192
- route = this._matchRoute(url) || this._matchRoute(this.fallback);
193
- plugins = this._collectPlugins(route);
194
- log('Redirecting', { context: this.context, route: this.route });
185
+ let redirecting;
186
+ do {
187
+ redirecting = false;
188
+ for (const plugin of plugins) {
189
+ try {
190
+ const result = await plugin?.shouldNavigate?.(this.context);
191
+ if (result) {
192
+ const condition = await result.condition();
193
+ if (!condition) {
194
+ url = new URL(result.redirect, this.baseUrl);
195
+ route = this._matchRoute(url) || this._matchRoute(this.fallback);
196
+ plugins = this._collectPlugins(route);
197
+ log("Redirecting", { context: this.context, route: this.route });
198
+ redirecting = true;
199
+ break;
200
+ }
195
201
  }
202
+ } catch (e) {
203
+ log(`Plugin "${plugin.name}" error on shouldNavigate hook`, e);
204
+ throw e;
196
205
  }
197
- } catch(e) {
198
- log(`Plugin "${plugin.name}" error on shouldNavigate hook`, e);
199
- throw e;
200
206
  }
201
- }
207
+ } while (redirecting);
202
208
 
203
209
  this.route = route;
204
210
 
@@ -3,12 +3,13 @@ export interface Config {
3
3
  plugins?: Plugin[];
4
4
  routes: RouteDefinition[];
5
5
  }
6
+ export interface ShouldNavigateResult {
7
+ redirect: string;
8
+ condition: (() => boolean) | (() => Promise<boolean>);
9
+ }
6
10
  export interface Plugin {
7
11
  name: string;
8
- shouldNavigate?: (context: Context) => {
9
- redirect: string;
10
- condition: () => boolean | (() => Promise<Boolean>);
11
- };
12
+ shouldNavigate?: ((context: Context) => ShouldNavigateResult) | ((context: Context) => Promise<ShouldNavigateResult>);
12
13
  beforeNavigation?: (context: Context) => void;
13
14
  afterNavigation?: (context: Context) => void;
14
15
  }
package/api/README.md DELETED
@@ -1,443 +0,0 @@
1
- # Api
2
-
3
- Small wrapper around the native [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API.
4
-
5
- ## Install
6
-
7
- ```
8
- npm i -S @thepassle/app-tools
9
- ```
10
-
11
- ## Usage
12
-
13
- ```js
14
- import { Api } from '@thepassle/app-tools/api.js';
15
-
16
- /** Using defaults: */
17
- const api = new Api();
18
-
19
- /** Or with configuration: */
20
- const api = new Api({
21
- baseURL: 'https://api.foo.com',
22
- responseType: 'text',
23
- plugins: [
24
- {
25
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
26
- afterFetch: (res) => res,
27
- transform: (data) => data,
28
- handleError: (e) => true
29
- }
30
- ]
31
- });
32
-
33
- const user = await api.get('/users/1');
34
- await api.post('/form/submit', { name: 'John Doe', email: 'johndoe@internet.com' });
35
-
36
- try {
37
- await api.get('/foo');
38
- } catch(e) {
39
- console.log(e); // StatusError
40
- e.message; // the `statusText` of the `response`
41
- e.response; // access the `response`
42
- }
43
- ```
44
-
45
- ### Error Handling
46
-
47
- By default `api` will throw an error if a response is not ok (`!response.ok`). If this is the case, it will throw a `StatusError`. The `StatusError` is thrown with the `response.statusText` as message, and also has the actual `response` available on it: `e.response`.
48
-
49
- ## Composable
50
-
51
- Use plugins to customize your requests to fit your needs
52
-
53
- ### `logger`
54
-
55
- ```js
56
- import { logger, loggerPlugin } from '@thepassle/app-tools/api/plugins/logger.js';
57
-
58
- /** Logs metadata to the console */
59
- api.get(url, {plugins: [logger]});
60
-
61
- /** Or */
62
- const logger = loggerPlugin({collapsed: false});
63
- api.get(url, {plugins: [logger]});
64
- ```
65
-
66
- ### `cache`
67
-
68
- ```js
69
- import { cache, cachePlugin } from '@thepassle/app-tools/api/plugins/cache.js';
70
-
71
- /** Caches the response for a default of 10 minutes */
72
- api.get(url, {plugins: [cache]});
73
-
74
- /** Or */
75
- const cache = cachePlugin({maxAge: 1000});
76
- api.get(url, {plugins: [cache]});
77
- ```
78
-
79
- ### `debounce`
80
-
81
- ```js
82
- import { debounce, debouncePlugin } from '@thepassle/app-tools/api/plugins/debounce.js';
83
-
84
- /** Debounces the response for a default of 1000 ms */
85
- api.get(url, {plugins: [debounce]});
86
-
87
- /** Or */
88
- const debounce = debouncePlugin({maxAge: 2000});
89
- api.get(url, {plugins: [debounce]});
90
- ```
91
-
92
- **Note:** The `debounce` plugin wraps the `fetchFn` in a debouncer. `await`ing the call will cause the debounce to be awaited. E.g.:
93
-
94
- ```js
95
- api.get(url, {plugins: [debounce]}).then(() => { console.log(1) });
96
- api.get(url, {plugins: [debounce]}).then(() => { console.log(2) });
97
-
98
- // Output:
99
- // 2
100
- ```
101
-
102
- But awaiting it will become:
103
- ```js
104
- await api.get(url, {plugins: [debounce]}).then(() => { console.log(1) });
105
- await api.get(url, {plugins: [debounce]}).then(() => { console.log(2) });
106
-
107
- // Output:
108
- // 1
109
- // 2
110
- ```
111
-
112
- ### `abort`
113
-
114
- ```js
115
- import { abort } from '@thepassle/app-tools/api/plugins/abort.js';
116
-
117
- /** Aborts previous, unfinished requests via an AbortController if requests are fired in quick succession, like spammy clicks on buttons */
118
- api.get(url, {plugins: [abort]});
119
- ```
120
-
121
- ### `mock`, `delay`
122
-
123
- ```js
124
- import { mock } from '@thepassle/app-tools/api/plugins/mock.js';
125
- import { delay, delayPlugin } from '@thepassle/app-tools/api/plugins/delay.js';
126
-
127
- /** Easily mock requests during development using the native `Response` object */
128
- api.get(url, {
129
- plugins: [
130
- mock(() => new Response(JSON.stringify({foo: 'bar'}))),
131
- delay // defaults to 1000ms
132
- ]
133
- });
134
-
135
- /** Or */
136
- const delay = delayPlugin(2000);
137
- api.get(url, {plugins: [delay]});
138
- ```
139
-
140
- ### `jsonPrefix`
141
-
142
- ```js
143
- import { jsonPrefix, jsonPrefixPlugin } from '@thepassle/app-tools/api/plugins/jsonPrefix.js';
144
-
145
- /** Add plugins to run on all requests */
146
- const api = new Api({ plugins: [jsonPrefix] });
147
-
148
- /** Or */
149
- const jsonPrefix = jsonPrefixPlugin('<prefix>');
150
- const api = new Api({ plugins: [jsonPRefix] });
151
- ```
152
-
153
- ### `xsrf`
154
-
155
- ```js
156
- import { xsrf, xsrfPlugin } from '@thepassle/app-tools/api/plugins/xsrf.js';
157
-
158
- /** Add plugins to run on all requests */
159
- const api = new Api({ plugins: [xsrf] });
160
-
161
- /** Or */
162
- const xsrf = xsrfPlugin({
163
- xsrfCookieName: '',
164
- xsrfHeaderName: ''
165
- });
166
- const api = new Api({ plugins: [xsrf] });
167
- ```
168
-
169
- ### Other
170
-
171
- ```js
172
- import { logger } from '@thepassle/app-tools/api/plugins/logger.js';
173
-
174
- /** Add plugins to run on all requests */
175
- const api = new Api({ plugins: [logoutOnUnauthorized, logger] });
176
- ```
177
-
178
- ## Methods
179
-
180
- ```js
181
- api.get(url, opts);
182
- api.options(url, opts);
183
- api.delete(url, opts);
184
- api.head(url, opts);
185
- api.post(url, data, opts);
186
- api.put(url, data, opts);
187
- api.patch(url, data, opts);
188
-
189
- api.addPlugin({
190
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
191
- afterFetch: (res) => res,
192
- transform: (data) => data,
193
- handleError: (e) => true
194
- });
195
- ```
196
-
197
- ## Big List of Options
198
-
199
- ```js
200
- api.get(url, {
201
- baseURL: 'https://api.foo.com',
202
- responseType: 'text',
203
- params: { foo: 'bar' },
204
- plugins: [
205
- {
206
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
207
- afterFetch: (res) => res,
208
- transform: (data) => data,
209
- handleError: (e) => true
210
- }
211
- ],
212
-
213
- // Also supports all the options of the native fetch API
214
- // mode, credentials, cache, redirect, referrer, integrity, keepalive, signal, referrerPolicy, headers, method
215
- });
216
- ```
217
-
218
- ## Options
219
-
220
- ### `baseURL`
221
-
222
- BaseURL to resolve all requests from. Can be set globally when instantiating a new `Api` instance, or on a per request basis. When set on a per request basis, will override the globally set baseURL (if set)
223
-
224
- ```js
225
- api.get(url, { baseURL: 'https://api.foo.com' });
226
- ```
227
-
228
- ### `responseType`
229
-
230
- Overwrite the default responseType (`'json'`)
231
-
232
- ```js
233
- api.get(url, { responseType: 'text' });
234
- ```
235
-
236
- ### `params`
237
-
238
- An object to be queryParam-ified and added to the request url
239
-
240
- ```js
241
- api.get(url, { params: { foo: 'bar' } });
242
- ```
243
-
244
- ### `plugins`
245
-
246
- An array of plugins.
247
-
248
- ```js
249
- api.get(url, {
250
- plugins: [
251
- {
252
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
253
- afterFetch: (res) => res,
254
- transform: (data) => data,
255
- handleError: (e) => true
256
- }
257
- ]
258
- })
259
- ```
260
-
261
- ## Plugins
262
-
263
- You can also use plugins. You can add plugins on a per-request basis, or you can globally add them to your `api` instance:
264
-
265
- ```js
266
- const api = new Api({
267
- plugins: [
268
- {
269
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
270
- afterFetch: (res) => res,
271
- transform: (data) => data,
272
- handleError: (e) => true
273
- }
274
- ]
275
- });
276
- ```
277
-
278
- You can also dynamically add plugins:
279
-
280
- ```js
281
- api.addPlugin({
282
- afterFetch: (res) => res
283
- });
284
- ```
285
-
286
- Or you can add them on a per request basis:
287
-
288
- ```js
289
- api.get(url, {
290
- plugins: [
291
- {
292
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
293
- afterFetch: (res) => res,
294
- transform: (data) => data,
295
- handleError: (e) => true
296
- }
297
- ]
298
- });
299
- ```
300
-
301
- ### `beforeFetch`
302
-
303
- Run logic before the actual `fetch` call happens, or alter/modify the meta information of a request.
304
- If you want to alter or modify the meta information of a request, make sure to return the value.
305
-
306
- ```js
307
- api.get('/foo', {
308
- plugins: [{
309
- beforeFetch: (meta) => ({...meta, url: '/bar'})
310
- }]
311
- });
312
-
313
- // RESULT: url `/bar` gets called instead of `/foo`
314
- ```
315
-
316
- If you dont want to alter or modify any meta information of the request, you dont have to return anything.
317
- ```js
318
- {
319
- beforeFetch: ({url}) => {
320
- console.log(url)
321
- }
322
- }
323
- ```
324
-
325
- ### `afterFetch`
326
-
327
- Runs immediately after the `fetch` call happened. `afterFetch` should always return a `Response`:
328
- ```js
329
- { afterFetch: (res) => res; }
330
- { afterFetch: (res) => new Response(JSON.stringify({foo: 'bar'}), res); }
331
- ```
332
-
333
- ### `transform`
334
-
335
- Runs after the `Response` object has been handled according to the `responseType`, (e.g.: `res.json()`). Can be used to transform the returned data:
336
- Should always return the data.
337
-
338
- ```js
339
- { transform: (data) => data }
340
- ```
341
-
342
- ### `handleError`
343
-
344
- Whether or not an error should throw. Return `true` if an error should throw, return `false` if an error should be ignored.
345
-
346
- ```js
347
- { handleError: (e) => e.message !== 'AbortError' }
348
- ```
349
-
350
- ### Plugin Examples
351
-
352
- #### Request logger
353
-
354
- ```js
355
- function requestLogger() {
356
- let start;
357
- return {
358
- beforeFetch: () => {
359
- start = Date.now();
360
- },
361
- afterFetch: () => {
362
- console.log(`Request took ${Date.now() - start}ms`);
363
- }
364
- }
365
- }
366
-
367
- api.addPlugin(requestLogger());
368
- ```
369
-
370
- #### Automatic logout on 401 or 403
371
-
372
- ```js
373
- api.get(url, {
374
- plugins: [
375
- {
376
- beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
377
- afterFetch: (res) => {
378
- if(res.status === 401 || res.status === 403) {
379
- logout();
380
- }
381
- return res;
382
- }
383
- }
384
- ]
385
- });
386
- ```
387
-
388
- #### Accessing the response body in `afterFetch`
389
-
390
- If you want to access the response body of your response in a plugin, make sure to clone the response:
391
-
392
- ```js
393
- const myPlugin = {
394
- afterFetch: async (originalResponse) => {
395
- const clone = originalResponse.clone();
396
- let data = await clone.text(); // or `.json()` etc
397
-
398
- data = data.replaceAll('foo', 'bar');
399
-
400
- // Always make sure to return a `Response`
401
- return new Response(data, originalResponse);
402
- }
403
- }
404
-
405
- api.addPlugin(myPlugin);
406
- ```
407
-
408
- #### Returning a new response entirely
409
-
410
- You can also overwrite the response entirely by returning a new `Response`
411
-
412
- ```js
413
- api.addPlugin({
414
- afterFetch: async (res) => new Response(JSON.stringify({foo: 'bar'}), res);
415
- });
416
- ```
417
-
418
- #### Overwriting the `fetch` implementation
419
-
420
- You can also overwrite the `fetch` implementation to use:
421
-
422
- ```js
423
- api.addPlugin({
424
- beforeFetch: (meta) => ({
425
- ...meta,
426
- fetchFn: () => Promise.resolve(new Response('{}'))
427
- })
428
- })
429
- ```
430
-
431
- Do note that if you use multiple plugins that overwrite the `fetchFn`, the last plugin to overwrite the `fetchFn` will win, there can only be one `fetchFn`.
432
-
433
- #### Transforming data
434
-
435
- ```js
436
- api.addPlugin({
437
- // Adds a `.foo` property to all of your response data
438
- transform: (data) => {
439
- data.foo = 'bar';
440
- return data;
441
- }
442
- })
443
- ```