inertia-sails 1.3.3 → 1.4.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 +55 -2
- package/index.js +117 -80
- package/lib/handle-bad-request.js +20 -6
- package/lib/helpers/build-page-object.js +117 -12
- package/lib/helpers/inertia-headers.js +4 -1
- package/lib/helpers/is-inertia-partial-request.js +10 -0
- package/lib/helpers/is-inertia-request.js +9 -0
- package/lib/helpers/request-context.js +49 -8
- package/lib/helpers/resolve-asset-version.js +12 -0
- package/lib/helpers/resolve-validation-errors.js +12 -5
- package/lib/location.js +11 -0
- package/lib/middleware/inertia-middleware.js +7 -2
- package/lib/props/always-prop.js +6 -2
- package/lib/props/defer-prop.js +46 -6
- package/lib/props/get-partial-data.js +9 -0
- package/lib/props/merge-prop.js +7 -4
- package/lib/props/merge-targets.js +114 -0
- package/lib/props/mergeable-prop.js +87 -0
- package/lib/props/once-prop.js +9 -1
- package/lib/props/optional-prop.js +12 -4
- package/lib/props/pick-props-to-resolve.js +14 -1
- package/lib/props/resolve-deferred-props.js +17 -1
- package/lib/props/resolve-except-props.js +11 -0
- package/lib/props/resolve-merge-props.js +81 -20
- package/lib/props/resolve-once-props.js +17 -7
- package/lib/props/resolve-only-props.js +10 -3
- package/lib/props/resolve-page-props.js +72 -13
- package/lib/props/resolve-scroll-props.js +17 -3
- package/lib/props/scroll-prop.js +30 -8
- package/lib/render.js +68 -9
- package/lib/responses/server-error.js +23 -6
- package/lib/types.js +68 -0
- package/package.json +3 -2
- package/test.js +53 -12
- package/tests/helpers/build-page-object.test.js +183 -0
- package/tests/helpers/preserve-fragment.test.js +97 -0
- package/tests/props/merge-targets.test.js +74 -0
- package/tests/props/resolve-merge-props.test.js +151 -0
- package/tests/props/resolve-page-props.test.js +70 -0
- package/tests/props/resolve-scroll-props.test.js +51 -0
- package/tests/render.test.js +197 -0
package/README.md
CHANGED
|
@@ -38,7 +38,10 @@ module.exports.inertia = {
|
|
|
38
38
|
<%- shipwright.styles() %>
|
|
39
39
|
</head>
|
|
40
40
|
<body>
|
|
41
|
-
<div id="app"
|
|
41
|
+
<div id="app"></div>
|
|
42
|
+
<script type="application/json" data-page="app">
|
|
43
|
+
<%- JSON.stringify(page).replace(/</g, '\\u003c') %>
|
|
44
|
+
</script>
|
|
42
45
|
<%- shipwright.scripts() %>
|
|
43
46
|
</body>
|
|
44
47
|
</html>
|
|
@@ -87,6 +90,19 @@ Return a URL string to redirect:
|
|
|
87
90
|
return '/dashboard'
|
|
88
91
|
```
|
|
89
92
|
|
|
93
|
+
#### Preserving URL fragments
|
|
94
|
+
|
|
95
|
+
When a standard Inertia redirect should carry the current hash to the next
|
|
96
|
+
page, mark the redirect before returning the URL:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
sails.inertia.preserveFragment()
|
|
100
|
+
return '/articles/new-slug'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If the user started from `/articles/old-slug#comments`, the Inertia client can
|
|
104
|
+
carry `#comments` to the redirected page.
|
|
105
|
+
|
|
90
106
|
### Sharing Data
|
|
91
107
|
|
|
92
108
|
#### `share(key, value)`
|
|
@@ -191,6 +207,25 @@ return {
|
|
|
191
207
|
}
|
|
192
208
|
```
|
|
193
209
|
|
|
210
|
+
Deferred props can also be rescued when a non-critical callback fails:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
return {
|
|
214
|
+
page: 'dashboard',
|
|
215
|
+
props: {
|
|
216
|
+
analytics: sails.inertia
|
|
217
|
+
.defer(async () => {
|
|
218
|
+
return await Analytics.getExpensiveReport()
|
|
219
|
+
})
|
|
220
|
+
.rescue()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
When a rescued deferred prop throws, it is omitted from `props` and its key is
|
|
226
|
+
reported in `rescuedProps`, allowing the client `<Deferred>` component to show
|
|
227
|
+
its rescue slot instead of failing the whole deferred response.
|
|
228
|
+
|
|
194
229
|
### Merge Props
|
|
195
230
|
|
|
196
231
|
Merge with existing client-side data (useful for infinite scroll):
|
|
@@ -199,13 +234,29 @@ Merge with existing client-side data (useful for infinite scroll):
|
|
|
199
234
|
// Shallow merge
|
|
200
235
|
messages: sails.inertia.merge(() => newMessages)
|
|
201
236
|
|
|
237
|
+
// Prepend new items instead of appending
|
|
238
|
+
notifications: sails.inertia.merge(() => newNotifications).prepend()
|
|
239
|
+
|
|
240
|
+
// Merge a nested array inside a paginated object
|
|
241
|
+
users: sails.inertia.merge(() => paginatedUsers).append('data')
|
|
242
|
+
|
|
243
|
+
// Match existing items by ID when merging
|
|
244
|
+
users: sails.inertia
|
|
245
|
+
.merge(() => paginatedUsers)
|
|
246
|
+
.append('data', {
|
|
247
|
+
matchOn: 'id'
|
|
248
|
+
})
|
|
249
|
+
|
|
202
250
|
// Deep merge (nested objects)
|
|
203
251
|
settings: sails.inertia.deepMerge(() => updatedSettings)
|
|
252
|
+
|
|
253
|
+
// Deep merge with item matching
|
|
254
|
+
chat: sails.inertia.deepMerge(() => chatState).matchOn('messages.id')
|
|
204
255
|
```
|
|
205
256
|
|
|
206
257
|
### Infinite Scroll
|
|
207
258
|
|
|
208
|
-
Paginate data with automatic merge behavior. Works with Inertia
|
|
259
|
+
Paginate data with automatic merge behavior. Works with Inertia's `<InfiniteScroll>` component:
|
|
209
260
|
|
|
210
261
|
```js
|
|
211
262
|
// Controller
|
|
@@ -244,6 +295,8 @@ defineProps({ invoices: Object })
|
|
|
244
295
|
</template>
|
|
245
296
|
```
|
|
246
297
|
|
|
298
|
+
`scroll()` targets the wrapped array for merging, such as `invoices.data`, and follows Inertia's infinite-scroll merge intent header so previous-page requests prepend while next-page requests append.
|
|
299
|
+
|
|
247
300
|
### History Encryption
|
|
248
301
|
|
|
249
302
|
Encrypt sensitive data in browser history:
|
package/index.js
CHANGED
|
@@ -9,32 +9,31 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @typedef {import('
|
|
13
|
-
* @typedef {import('
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @typedef {
|
|
18
|
-
* @
|
|
19
|
-
* @
|
|
20
|
-
* @
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {Object} InertiaPageProps
|
|
26
|
-
* @property {Object.<string, *>} [props] - Page props to pass to the component
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
12
|
+
* @typedef {import('./lib/types').InertiaRequest} Request
|
|
13
|
+
* @typedef {import('./lib/types').InertiaResponse} Response
|
|
14
|
+
* @typedef {import('./lib/types').InertiaProps} InertiaProps
|
|
15
|
+
* @typedef {import('./lib/types').SailsLike} SailsLike
|
|
16
|
+
* @typedef {import('./lib/types').PropCallback} PropCallback
|
|
17
|
+
* @typedef {import('./lib/types').BadRequestData} BadRequestData
|
|
18
|
+
* @typedef {(req: Request, res: Response, next: () => any) => any} Middleware
|
|
19
|
+
* @typedef {Record<string, any>} InertiaHook
|
|
20
|
+
* @typedef {{ message?: string, stack?: string, name?: string }} ErrorLike
|
|
21
|
+
*
|
|
30
22
|
* @typedef {Object} InertiaRenderData
|
|
31
23
|
* @property {string} page - The component name to render
|
|
32
|
-
* @property {
|
|
33
|
-
* @property {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
24
|
+
* @property {InertiaProps} [props] - Props to pass to the component
|
|
25
|
+
* @property {InertiaProps} [locals] - Additional locals for the root EJS template
|
|
26
|
+
*
|
|
27
|
+
* @typedef {Object} DeferOptions
|
|
28
|
+
* @property {boolean} [rescue=false] - Rescue callback failures
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} ScrollOptions
|
|
31
|
+
* @property {number} [page=0] - Current page index (0-based)
|
|
32
|
+
* @property {number} [perPage=10] - Items per page
|
|
33
|
+
* @property {number} [total=0] - Total number of items
|
|
34
|
+
* @property {string} [pageName='page'] - Query parameter name for pagination
|
|
35
|
+
* @property {string} [wrapper='data'] - Key to wrap the data in
|
|
36
|
+
* @property {string|null} [matchOn] - Optional field used to match items when merging
|
|
38
37
|
*/
|
|
39
38
|
|
|
40
39
|
const inertia = require('./lib/middleware/inertia-middleware')
|
|
@@ -51,7 +50,12 @@ const ScrollProp = require('./lib/props/scroll-prop')
|
|
|
51
50
|
const handleBadRequest = require('./lib/handle-bad-request')
|
|
52
51
|
const handleServerError = require('./lib/responses/server-error')
|
|
53
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @param {SailsLike} sails
|
|
55
|
+
* @returns {InertiaHook}
|
|
56
|
+
*/
|
|
54
57
|
module.exports = function defineInertiaHook(sails) {
|
|
58
|
+
/** @type {InertiaHook} */
|
|
55
59
|
let hook
|
|
56
60
|
const routesToBindInertiaTo = [
|
|
57
61
|
'GET r|^((?![^?]*\\/[^?\\/]+\\.[^?\\/]+(\\?.*)?).)*$|',
|
|
@@ -66,6 +70,12 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
66
70
|
// Using startup timestamp ensures fresh assets on each server restart
|
|
67
71
|
const startupVersion = Date.now().toString(36)
|
|
68
72
|
|
|
73
|
+
/** @type {Middleware} */
|
|
74
|
+
const runWithRequestContext = (req, res, next) => {
|
|
75
|
+
if (requestContext.getContext()) return next()
|
|
76
|
+
requestContext.run(req, res, next)
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Get asset version from Shipwright manifest.
|
|
71
81
|
* Automatically hashes the manifest content for cache busting.
|
|
@@ -150,9 +160,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
150
160
|
}
|
|
151
161
|
}
|
|
152
162
|
if (!mw.inertiaContext) {
|
|
153
|
-
mw.inertiaContext =
|
|
154
|
-
requestContext.run(req, res, next)
|
|
155
|
-
}
|
|
163
|
+
mw.inertiaContext = runWithRequestContext
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
},
|
|
@@ -182,28 +190,12 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
182
190
|
before: {
|
|
183
191
|
'GET /*': {
|
|
184
192
|
skipAssets: true,
|
|
185
|
-
fn:
|
|
186
|
-
// Skip if context already set up by HTTP middleware
|
|
187
|
-
if (requestContext.getContext()) return next()
|
|
188
|
-
requestContext.run(req, res, next)
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
'POST /*': (req, res, next) => {
|
|
192
|
-
if (requestContext.getContext()) return next()
|
|
193
|
-
requestContext.run(req, res, next)
|
|
194
|
-
},
|
|
195
|
-
'PUT /*': (req, res, next) => {
|
|
196
|
-
if (requestContext.getContext()) return next()
|
|
197
|
-
requestContext.run(req, res, next)
|
|
193
|
+
fn: runWithRequestContext
|
|
198
194
|
},
|
|
199
|
-
'
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
'DELETE /*': (req, res, next) => {
|
|
204
|
-
if (requestContext.getContext()) return next()
|
|
205
|
-
requestContext.run(req, res, next)
|
|
206
|
-
}
|
|
195
|
+
'POST /*': runWithRequestContext,
|
|
196
|
+
'PUT /*': runWithRequestContext,
|
|
197
|
+
'PATCH /*': runWithRequestContext,
|
|
198
|
+
'DELETE /*': runWithRequestContext
|
|
207
199
|
}
|
|
208
200
|
},
|
|
209
201
|
|
|
@@ -325,7 +317,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
325
317
|
* Create an optional prop
|
|
326
318
|
* This allows you to define properties that are only evaluated when accessed.
|
|
327
319
|
* @docs https://docs.sailscasts.com/boring-stack/partial-reloads#lazy-data-evaluation
|
|
328
|
-
* @param {
|
|
320
|
+
* @param {PropCallback} callback - The callback function to execute
|
|
329
321
|
* @returns {OptionalProp} - The optional prop
|
|
330
322
|
*/
|
|
331
323
|
optional(callback) {
|
|
@@ -336,7 +328,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
336
328
|
* Create a mergeable prop
|
|
337
329
|
* This allows you to merge multiple props together.
|
|
338
330
|
* @docs https://docs.sailscasts.com/boring-stack/merging-props
|
|
339
|
-
* @param {
|
|
331
|
+
* @param {PropCallback} callback - The callback function to execute
|
|
340
332
|
* @returns {MergeProp} - The mergeable prop
|
|
341
333
|
*/
|
|
342
334
|
merge(callback) {
|
|
@@ -347,7 +339,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
347
339
|
* Create an always prop
|
|
348
340
|
* Always props are resolved on every request, whether partial or not.
|
|
349
341
|
* @docs https://docs.sailscasts.com/boring-stack/partial-reloads#lazy-data-evaluation
|
|
350
|
-
* @param {
|
|
342
|
+
* @param {PropCallback} callback - The callback function
|
|
351
343
|
* @returns {AlwaysProp} - The always prop
|
|
352
344
|
*/
|
|
353
345
|
always(callback) {
|
|
@@ -357,12 +349,13 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
357
349
|
* Create a deferred prop
|
|
358
350
|
* This allows you to load certain page data after the initial render.
|
|
359
351
|
* @docs https://docs.sailscasts.com/boring-stack/deferred-props
|
|
360
|
-
* @param {
|
|
361
|
-
* @param {string} group - The group name
|
|
352
|
+
* @param {PropCallback} cb - The callback function to execute
|
|
353
|
+
* @param {string|DeferOptions} group - The group name, or options when no group is needed
|
|
354
|
+
* @param {DeferOptions} options - Deferred prop options
|
|
362
355
|
* @returns {DeferProp} - The deferred prop
|
|
363
356
|
*/
|
|
364
|
-
defer(cb, group = 'default') {
|
|
365
|
-
return new DeferProp(cb, group)
|
|
357
|
+
defer(cb, group = 'default', options = {}) {
|
|
358
|
+
return new DeferProp(cb, group, options)
|
|
366
359
|
},
|
|
367
360
|
|
|
368
361
|
/**
|
|
@@ -371,7 +364,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
371
364
|
* The client tracks which props it has via X-Inertia-Except-Once-Props header.
|
|
372
365
|
* Useful for expensive computations that don't change often.
|
|
373
366
|
* @docs https://docs.sailscasts.com/boring-stack/once-props
|
|
374
|
-
* @param {
|
|
367
|
+
* @param {PropCallback} callback - The callback function to execute
|
|
375
368
|
* @returns {OnceProp} - The once prop
|
|
376
369
|
* @example
|
|
377
370
|
* // Basic usage
|
|
@@ -394,7 +387,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
394
387
|
* Combines share() and once() - the prop is shared and only resolved once.
|
|
395
388
|
* @docs https://docs.sailscasts.com/boring-stack/once-props#share-once
|
|
396
389
|
* @param {string} key - The key of the property
|
|
397
|
-
* @param {
|
|
390
|
+
* @param {PropCallback} callback - The callback function to execute
|
|
398
391
|
* @returns {OnceProp} - The once prop (for chaining)
|
|
399
392
|
* @example
|
|
400
393
|
* // In a policy or middleware
|
|
@@ -445,7 +438,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
445
438
|
* This prevents "phantom" toasts/notifications when users navigate back.
|
|
446
439
|
* Flash data is stored in the session so it persists across redirects.
|
|
447
440
|
* @docs https://docs.sailscasts.com/boring-stack/flash
|
|
448
|
-
* @param {string|
|
|
441
|
+
* @param {string|Record<string, any>} key - The key or an object of key-value pairs
|
|
449
442
|
* @param {*} [value] - The value (if key is a string)
|
|
450
443
|
* @returns {Object} - The hook instance for chaining
|
|
451
444
|
* @example
|
|
@@ -469,7 +462,8 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
469
462
|
if (typeof key === 'object' && key !== null) {
|
|
470
463
|
req.session._inertiaFlash = { ...req.session._inertiaFlash, ...key }
|
|
471
464
|
} else {
|
|
472
|
-
|
|
465
|
+
const flashKey = /** @type {string} */ (key)
|
|
466
|
+
req.session._inertiaFlash[flashKey] = value
|
|
473
467
|
}
|
|
474
468
|
return this
|
|
475
469
|
},
|
|
@@ -486,8 +480,8 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
486
480
|
/**
|
|
487
481
|
* Consume and clear flash data from the session.
|
|
488
482
|
* Called internally by build-page-object after adding to response.
|
|
489
|
-
* @param {
|
|
490
|
-
* @returns {
|
|
483
|
+
* @param {Request} req - The request object
|
|
484
|
+
* @returns {InertiaProps} - The flash data that was consumed
|
|
491
485
|
*/
|
|
492
486
|
consumeFlash(req) {
|
|
493
487
|
const flash = req?.session?._inertiaFlash || {}
|
|
@@ -501,7 +495,7 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
501
495
|
* Create a deep merge prop
|
|
502
496
|
* Like merge(), but recursively merges nested objects instead of replacing them.
|
|
503
497
|
* @docs https://docs.sailscasts.com/boring-stack/merging-props#deep-merge
|
|
504
|
-
* @param {
|
|
498
|
+
* @param {PropCallback} callback - The callback function to execute
|
|
505
499
|
* @returns {MergeProp} - The mergeable prop with deep merge enabled
|
|
506
500
|
* @example
|
|
507
501
|
* // Deep merge nested user preferences
|
|
@@ -515,9 +509,9 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
515
509
|
|
|
516
510
|
/**
|
|
517
511
|
* Render the response
|
|
518
|
-
* @param {
|
|
519
|
-
* @param {
|
|
520
|
-
* @param {
|
|
512
|
+
* @param {Request} req - The request object
|
|
513
|
+
* @param {Response} res - The response object
|
|
514
|
+
* @param {InertiaRenderData} data - The data to render
|
|
521
515
|
* @returns {*} - The rendered response
|
|
522
516
|
*/
|
|
523
517
|
render(req, res, data) {
|
|
@@ -527,8 +521,8 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
527
521
|
* Handle Inertia redirects (external URLs or non-Inertia pages)
|
|
528
522
|
* Forces a full page visit instead of an Inertia XHR request.
|
|
529
523
|
* See https://docs.sailscasts.com/boring-stack/redirects
|
|
530
|
-
* @param {
|
|
531
|
-
* @param {
|
|
524
|
+
* @param {Request} req - The request object
|
|
525
|
+
* @param {Response} res - The response object
|
|
532
526
|
* @param {string} url - The URL to redirect to
|
|
533
527
|
* @returns {Object} - The response object with the redirect
|
|
534
528
|
*/
|
|
@@ -585,12 +579,60 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
585
579
|
return requestContext.getClearHistory()
|
|
586
580
|
},
|
|
587
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Preserve the current URL fragment across a standard Inertia redirect.
|
|
584
|
+
* The flag is stored in the session so it survives the redirect request,
|
|
585
|
+
* then it is consumed by the next Inertia page response.
|
|
586
|
+
*
|
|
587
|
+
* @docs https://docs.sailscasts.com/boring-stack/redirects#preserving-fragments
|
|
588
|
+
* @param {boolean} preserve - Whether to preserve the URL fragment
|
|
589
|
+
* @returns {Object} - The hook instance for chaining
|
|
590
|
+
* @example
|
|
591
|
+
* sails.inertia.preserveFragment()
|
|
592
|
+
* return '/article/new-slug'
|
|
593
|
+
*/
|
|
594
|
+
preserveFragment(preserve = true) {
|
|
595
|
+
const context = requestContext.getContext()
|
|
596
|
+
const req = requestContext.getRequest()
|
|
597
|
+
|
|
598
|
+
if (context) {
|
|
599
|
+
requestContext.setPreserveFragment(preserve)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (req?.session) {
|
|
603
|
+
if (preserve) {
|
|
604
|
+
req.session._inertiaPreserveFragment = true
|
|
605
|
+
} else {
|
|
606
|
+
delete req.session._inertiaPreserveFragment
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return this
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Consume the preserve fragment flag for the current request.
|
|
615
|
+
* @param {Request} req - The request object
|
|
616
|
+
* @returns {boolean} - Whether to preserve the URL fragment
|
|
617
|
+
*/
|
|
618
|
+
consumePreserveFragment(req) {
|
|
619
|
+
const preserve =
|
|
620
|
+
requestContext.getPreserveFragment() ||
|
|
621
|
+
Boolean(req?.session?._inertiaPreserveFragment)
|
|
622
|
+
|
|
623
|
+
if (req?.session) {
|
|
624
|
+
delete req.session._inertiaPreserveFragment
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return preserve
|
|
628
|
+
},
|
|
629
|
+
|
|
588
630
|
/**
|
|
589
631
|
* Handle bad request responses for Inertia.js
|
|
590
632
|
* For Inertia requests with validation errors, redirects back with errors in session.
|
|
591
|
-
* @param {
|
|
592
|
-
* @param {
|
|
593
|
-
* @param {
|
|
633
|
+
* @param {Request} req - The request object
|
|
634
|
+
* @param {Response} res - The response object
|
|
635
|
+
* @param {BadRequestData|Error|Record<string, any>} [optionalData] - Optional error data or Error object
|
|
594
636
|
* @returns {*} - Response (redirect for Inertia, status code for non-Inertia)
|
|
595
637
|
*/
|
|
596
638
|
handleBadRequest(req, res, optionalData) {
|
|
@@ -602,9 +644,9 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
602
644
|
* For Inertia requests in development, displays a styled error modal with stack trace.
|
|
603
645
|
* In production, redirects back with a flash error message.
|
|
604
646
|
* @docs https://docs.sailscasts.com/boring-stack/error-handling
|
|
605
|
-
* @param {
|
|
606
|
-
* @param {
|
|
607
|
-
* @param {
|
|
647
|
+
* @param {Request} req - The request object
|
|
648
|
+
* @param {Response} res - The response object
|
|
649
|
+
* @param {ErrorLike} [error] - Optional error data or Error object
|
|
608
650
|
* @returns {*} - Response (HTML modal for dev Inertia, redirect for prod)
|
|
609
651
|
*/
|
|
610
652
|
handleServerError(req, res, error) {
|
|
@@ -620,13 +662,8 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
620
662
|
* to 1-based for the Inertia client.
|
|
621
663
|
*
|
|
622
664
|
* @docs https://docs.sailscasts.com/boring-stack/infinite-scroll
|
|
623
|
-
* @param {
|
|
624
|
-
* @param {
|
|
625
|
-
* @param {number} [options.page=0] - Current page index (0-based)
|
|
626
|
-
* @param {number} [options.perPage=10] - Items per page
|
|
627
|
-
* @param {number} [options.total=0] - Total number of items
|
|
628
|
-
* @param {string} [options.pageName='page'] - Query parameter name for pagination
|
|
629
|
-
* @param {string} [options.wrapper='data'] - Key to wrap the data in
|
|
665
|
+
* @param {PropCallback} callback - Callback returning the paginated data array
|
|
666
|
+
* @param {ScrollOptions} [options] - Pagination options
|
|
630
667
|
* @returns {ScrollProp} - The scroll prop
|
|
631
668
|
* @example
|
|
632
669
|
* // Basic usage
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./types').InertiaRequest} InertiaRequest
|
|
3
|
+
* @typedef {import('./types').InertiaResponse} InertiaResponse
|
|
4
|
+
* @typedef {import('./types').BadRequestData} BadRequestData
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Handle bad request responses for Inertia.js
|
|
3
9
|
*
|
|
@@ -5,9 +11,9 @@
|
|
|
5
11
|
* previous page with errors stored in the session. For non-Inertia requests,
|
|
6
12
|
* it returns a standard 400 response.
|
|
7
13
|
*
|
|
8
|
-
* @param {
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {
|
|
14
|
+
* @param {InertiaRequest} req - Express/Sails request object
|
|
15
|
+
* @param {InertiaResponse} res - Express/Sails response object
|
|
16
|
+
* @param {BadRequestData|Error|Record<string, any>} [optionalData] - Optional error data or Error object
|
|
11
17
|
* @returns {*} - Response (redirect for Inertia, status code for non-Inertia)
|
|
12
18
|
*
|
|
13
19
|
* @example
|
|
@@ -22,13 +28,21 @@ module.exports = function handleBadRequest(req, res, optionalData) {
|
|
|
22
28
|
const statusCodeToSet = 400
|
|
23
29
|
|
|
24
30
|
// Check if it's an Inertia request
|
|
25
|
-
if (req.header('X-Inertia')) {
|
|
26
|
-
if (
|
|
31
|
+
if (req.header?.('X-Inertia')) {
|
|
32
|
+
if (
|
|
33
|
+
optionalData &&
|
|
34
|
+
!(optionalData instanceof Error) &&
|
|
35
|
+
Array.isArray(optionalData.problems)
|
|
36
|
+
) {
|
|
37
|
+
/** @type {Record<string, string[]>} */
|
|
27
38
|
const errors = {}
|
|
28
39
|
optionalData.problems.forEach((problem) => {
|
|
29
40
|
if (typeof problem === 'object') {
|
|
30
41
|
Object.keys(problem).forEach((propertyName) => {
|
|
31
|
-
const sanitizedProblem = problem[propertyName].replace(
|
|
42
|
+
const sanitizedProblem = String(problem[propertyName]).replace(
|
|
43
|
+
/\.$/,
|
|
44
|
+
''
|
|
45
|
+
) // Trim trailing dot
|
|
32
46
|
if (!errors[propertyName]) {
|
|
33
47
|
errors[propertyName] = [sanitizedProblem]
|
|
34
48
|
} else {
|
|
@@ -4,23 +4,103 @@ const resolveMergeProps = require('../props/resolve-merge-props')
|
|
|
4
4
|
const { resolveOncePropsMetadata } = require('../props/resolve-once-props')
|
|
5
5
|
const resolvePageProps = require('../props/resolve-page-props')
|
|
6
6
|
const resolveScrollProps = require('../props/resolve-scroll-props')
|
|
7
|
+
const resolveAssetVersion = require('./resolve-asset-version')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} InertiaHookApi
|
|
11
|
+
* @property {() => Object.<string, *>} getShared
|
|
12
|
+
* @property {() => boolean} shouldClearHistory
|
|
13
|
+
* @property {() => boolean} shouldEncryptHistory
|
|
14
|
+
* @property {(req: BuildPageObjectRequest) => boolean} consumePreserveFragment
|
|
15
|
+
* @property {(req: BuildPageObjectRequest) => Object.<string, *>} consumeFlash
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} SailsLike
|
|
20
|
+
* @property {Object.<string, *>} config
|
|
21
|
+
* @property {InertiaHookApi} inertia
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} BuildPageObjectRequest
|
|
26
|
+
* @property {string} [url]
|
|
27
|
+
* @property {string} [originalUrl]
|
|
28
|
+
* @property {SailsLike} _sails
|
|
29
|
+
* @property {(header: string) => string|undefined} get
|
|
30
|
+
*/
|
|
7
31
|
|
|
8
32
|
/**
|
|
9
33
|
* @typedef {Object} InertiaPageObject
|
|
10
34
|
* @property {string} component - The component name to render
|
|
11
35
|
* @property {string} url - The current URL
|
|
12
|
-
* @property {string|number} version - Asset version for cache busting
|
|
36
|
+
* @property {string|number|null} version - Asset version for cache busting
|
|
13
37
|
* @property {Object.<string, *>} props - Resolved page props
|
|
14
|
-
* @property {boolean} clearHistory - Whether to clear browser history
|
|
15
|
-
* @property {boolean} encryptHistory - Whether to encrypt history state
|
|
38
|
+
* @property {boolean} [clearHistory] - Whether to clear browser history
|
|
39
|
+
* @property {boolean} [encryptHistory] - Whether to encrypt history state
|
|
40
|
+
* @property {boolean} [preserveFragment] - Whether to preserve URL fragments across redirects
|
|
41
|
+
* @property {string[]} [sharedProps] - Shared prop keys included in this response
|
|
16
42
|
* @property {string[]} [mergeProps] - Props that should be merged on client
|
|
43
|
+
* @property {string[]} [prependProps] - Props that should be prepended on client
|
|
17
44
|
* @property {string[]} [deepMergeProps] - Props that should be deep merged
|
|
45
|
+
* @property {string[]} [matchPropsOn] - Prop paths to use for matching merge items
|
|
18
46
|
* @property {Object.<string, string[]>} [deferredProps] - Deferred props by group
|
|
47
|
+
* @property {string[]} [rescuedProps] - Deferred props rescued after callback failures
|
|
19
48
|
* @property {Object.<string, *>} [onceProps] - Once-prop metadata
|
|
20
49
|
* @property {Object.<string, *>} [scrollProps] - Scroll props for InfiniteScroll component
|
|
21
50
|
* @property {Object.<string, *>} [flash] - Flash data (not persisted in history)
|
|
22
51
|
*/
|
|
23
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {'mergeProps'|'prependProps'|'deepMergeProps'|'matchPropsOn'} PathMetadataKey
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {InertiaPageObject} page
|
|
59
|
+
* @param {PathMetadataKey} key
|
|
60
|
+
*/
|
|
61
|
+
function removeEmptyArrayMetadata(page, key) {
|
|
62
|
+
if (Array.isArray(page[key]) && page[key].length === 0) {
|
|
63
|
+
delete page[key]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {InertiaPageObject} page
|
|
69
|
+
* @param {string[]} rescuedProps
|
|
70
|
+
*/
|
|
71
|
+
function removeRescuedPropMetadata(page, rescuedProps) {
|
|
72
|
+
if (rescuedProps.length === 0) return
|
|
73
|
+
|
|
74
|
+
const rescuedRootProps = new Set(rescuedProps)
|
|
75
|
+
/** @param {string} path */
|
|
76
|
+
const isNotRescuedPath = (path) => !rescuedRootProps.has(path.split('.')[0])
|
|
77
|
+
/** @type {PathMetadataKey[]} */
|
|
78
|
+
const pathMetadataKeys = [
|
|
79
|
+
'mergeProps',
|
|
80
|
+
'prependProps',
|
|
81
|
+
'deepMergeProps',
|
|
82
|
+
'matchPropsOn'
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
pathMetadataKeys.forEach((key) => {
|
|
86
|
+
if (Array.isArray(page[key])) {
|
|
87
|
+
page[key] = page[key].filter(isNotRescuedPath)
|
|
88
|
+
removeEmptyArrayMetadata(page, key)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const scrollProps = page.scrollProps
|
|
93
|
+
if (scrollProps) {
|
|
94
|
+
rescuedProps.forEach((key) => {
|
|
95
|
+
delete scrollProps[key]
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (Object.keys(scrollProps).length === 0) {
|
|
99
|
+
delete page.scrollProps
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
24
104
|
/**
|
|
25
105
|
* Build the Inertia page object for a response.
|
|
26
106
|
*
|
|
@@ -30,42 +110,67 @@ const resolveScrollProps = require('../props/resolve-scroll-props')
|
|
|
30
110
|
* Uses request-scoped shared props (via AsyncLocalStorage) merged with
|
|
31
111
|
* global shared props to prevent data leaking between concurrent requests.
|
|
32
112
|
*
|
|
33
|
-
* @param {
|
|
113
|
+
* @param {BuildPageObjectRequest} req - Express/Sails request object
|
|
34
114
|
* @param {string} component - The component name to render
|
|
35
115
|
* @param {Object.<string, *>} pageProps - Props specific to this page
|
|
36
116
|
* @returns {Promise<InertiaPageObject>} - The complete page object
|
|
37
117
|
*/
|
|
38
118
|
module.exports = async function buildPageObject(req, component, pageProps) {
|
|
39
119
|
const sails = req._sails
|
|
40
|
-
let url = req.url || req.originalUrl
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
120
|
+
let url = req.url || req.originalUrl || '/'
|
|
121
|
+
const currentVersion = resolveAssetVersion(sails)
|
|
122
|
+
|
|
123
|
+
const sharedProps = sails.inertia.getShared()
|
|
124
|
+
const sharedPropKeys = Object.keys(sharedProps)
|
|
44
125
|
|
|
45
126
|
// Merge props: global shared → request-scoped shared → page-specific
|
|
46
127
|
// This ensures user-specific data (from share()) doesn't leak between requests
|
|
47
128
|
const allProps = {
|
|
48
|
-
...
|
|
129
|
+
...sharedProps, // Merges global + request-scoped
|
|
49
130
|
...pageProps
|
|
50
131
|
}
|
|
51
132
|
|
|
52
133
|
const propsToResolve = pickPropsToResolve(req, component, allProps)
|
|
134
|
+
const clearHistory = sails.inertia.shouldClearHistory()
|
|
135
|
+
const encryptHistory = sails.inertia.shouldEncryptHistory()
|
|
136
|
+
const preserveFragment = sails.inertia.consumePreserveFragment(req)
|
|
137
|
+
const resolvedPageProps = await resolvePageProps.withMetadata(propsToResolve)
|
|
53
138
|
|
|
54
139
|
// Build the page object with all metadata
|
|
55
140
|
// Use request-scoped history settings (prevents race conditions)
|
|
141
|
+
/** @type {InertiaPageObject} */
|
|
56
142
|
const page = {
|
|
57
143
|
component,
|
|
58
144
|
url,
|
|
59
145
|
version: currentVersion,
|
|
60
|
-
props:
|
|
61
|
-
clearHistory: sails.inertia.shouldClearHistory(),
|
|
62
|
-
encryptHistory: sails.inertia.shouldEncryptHistory(),
|
|
146
|
+
props: resolvedPageProps.props,
|
|
63
147
|
...resolveMergeProps(req, allProps),
|
|
64
148
|
...resolveDeferredProps(req, component, allProps),
|
|
65
149
|
...resolveOncePropsMetadata(allProps),
|
|
66
150
|
...resolveScrollProps(allProps)
|
|
67
151
|
}
|
|
68
152
|
|
|
153
|
+
if (clearHistory) {
|
|
154
|
+
page.clearHistory = true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (encryptHistory) {
|
|
158
|
+
page.encryptHistory = true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (preserveFragment) {
|
|
162
|
+
page.preserveFragment = true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (resolvedPageProps.rescuedProps.length > 0) {
|
|
166
|
+
page.rescuedProps = resolvedPageProps.rescuedProps
|
|
167
|
+
removeRescuedPropMetadata(page, resolvedPageProps.rescuedProps)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (sharedPropKeys.length > 0) {
|
|
171
|
+
page.sharedProps = sharedPropKeys
|
|
172
|
+
}
|
|
173
|
+
|
|
69
174
|
// Consume flash data from session and add to props
|
|
70
175
|
// Flash data is included in props.flash so it's accessible via usePage().props.flash
|
|
71
176
|
// Note: Unlike regular props, flash data should NOT be persisted in browser history
|