inertia-sails 1.4.1 → 1.5.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 +156 -1
- package/index.js +92 -0
- package/lib/handle-bad-request.js +51 -45
- package/lib/helpers/humanize-validation-errors.js +590 -0
- package/lib/helpers/inertia-headers.js +10 -1
- package/lib/helpers/precognition.js +150 -0
- package/lib/helpers/vary-header.js +26 -0
- package/lib/render.js +13 -1
- package/lib/responses/server-error.js +422 -288
- package/lib/ssr.js +200 -0
- package/lib/types.js +3 -1
- package/package.json +4 -2
- package/tests/handle-bad-request.test.js +318 -0
- package/tests/helpers/precognition.test.js +172 -0
- package/tests/render.test.js +121 -2
- package/tests/server-error.test.js +280 -0
package/README.md
CHANGED
|
@@ -194,6 +194,154 @@ sails.inertia.flash({ error: 'Failed', field: 'email' })
|
|
|
194
194
|
|
|
195
195
|
Access in your frontend via `page.props.flash`.
|
|
196
196
|
|
|
197
|
+
### Error Pages
|
|
198
|
+
|
|
199
|
+
In development, 500-level HTML responses render a rich Youch error page with
|
|
200
|
+
the stack trace and sanitized request metadata. For Inertia visits, the client
|
|
201
|
+
will show that HTML in its development error modal.
|
|
202
|
+
|
|
203
|
+
In production, `inertia-sails` renders the configured Inertia error page for
|
|
204
|
+
`403`, `404`, `500`, and `503` responses by default. The page receives
|
|
205
|
+
`status`, `title`, and `message` props:
|
|
206
|
+
|
|
207
|
+
```vue
|
|
208
|
+
<!-- assets/js/pages/error.vue -->
|
|
209
|
+
<script setup>
|
|
210
|
+
import { Head, Link } from '@inertiajs/vue3'
|
|
211
|
+
|
|
212
|
+
defineProps({
|
|
213
|
+
status: Number,
|
|
214
|
+
title: String,
|
|
215
|
+
message: String
|
|
216
|
+
})
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<template>
|
|
220
|
+
<Head :title="`${status} ${title}`" />
|
|
221
|
+
|
|
222
|
+
<main>
|
|
223
|
+
<p>Status {{ status }}</p>
|
|
224
|
+
<h1>{{ title }}</h1>
|
|
225
|
+
<p>{{ message }}</p>
|
|
226
|
+
<Link href="/">Go home</Link>
|
|
227
|
+
</main>
|
|
228
|
+
</template>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Wire Sails' standard responses through the hook so framework-level 403/404/500
|
|
232
|
+
responses use the same policy:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
// api/responses/serverError.js
|
|
236
|
+
module.exports = function serverError(error) {
|
|
237
|
+
return this.req._sails.inertia.handleServerError(this.req, this.res, error)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// api/responses/notFound.js
|
|
241
|
+
module.exports = function notFound(error) {
|
|
242
|
+
return this.req._sails.inertia.handleErrorPage(this.req, this.res, {
|
|
243
|
+
statusCode: 404,
|
|
244
|
+
error
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// api/responses/forbidden.js
|
|
249
|
+
module.exports = function forbidden(error) {
|
|
250
|
+
return this.req._sails.inertia.handleErrorPage(this.req, this.res, {
|
|
251
|
+
statusCode: 403,
|
|
252
|
+
error
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Hybrid apps can keep classic Sails EJS error views by disabling the Inertia
|
|
258
|
+
error page:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
// config/inertia.js
|
|
262
|
+
module.exports.inertia = {
|
|
263
|
+
errorPage: false
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Precognition
|
|
268
|
+
|
|
269
|
+
Inertia v3 forms can validate against server-owned Sails rules before the real
|
|
270
|
+
submit runs. `inertia-sails` handles the Precognition response headers and
|
|
271
|
+
returns validation failures as `422` JSON instead of redirecting through the
|
|
272
|
+
normal session-backed Inertia error flow.
|
|
273
|
+
|
|
274
|
+
Use `withPrecognition()` on the client:
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
const form = useForm({
|
|
278
|
+
email: null
|
|
279
|
+
}).withPrecognition('post', '/forgot-password')
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Then validate a field when the user leaves it:
|
|
283
|
+
|
|
284
|
+
```vue
|
|
285
|
+
<InputEmail v-model="form.email" @blur="form.validate('email')" />
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
On the server, add a small custom response so successful Precognition checks
|
|
289
|
+
can exit before side effects without going through the action's normal
|
|
290
|
+
`success` response type:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
// api/responses/precognitionSuccess.js
|
|
294
|
+
module.exports = function precognitionSuccess() {
|
|
295
|
+
return this.req._sails.inertia.handlePrecognitionSuccess(this.req, this.res)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Then use a named exit in the action:
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
exits: {
|
|
303
|
+
success: {
|
|
304
|
+
responseType: 'redirect'
|
|
305
|
+
},
|
|
306
|
+
precognitionSuccess: {
|
|
307
|
+
responseType: 'precognitionSuccess'
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
fn: async function ({ email }, exits) {
|
|
312
|
+
if (sails.inertia.isPrecognitive(this.req)) {
|
|
313
|
+
return exits.precognitionSuccess()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await sendPasswordResetEmail(email)
|
|
317
|
+
return '/check-email'
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
For custom database-backed checks, use `shouldValidate()` so expensive rules
|
|
322
|
+
only run when the client asked for that field:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
if (sails.inertia.shouldValidate('email', this.req)) {
|
|
326
|
+
const existingUser = await User.findOne({ email })
|
|
327
|
+
|
|
328
|
+
if (existingUser) {
|
|
329
|
+
throw {
|
|
330
|
+
badSignupRequest: {
|
|
331
|
+
problems: [{ email: 'An account with this email already exists.' }]
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Available helpers:
|
|
339
|
+
|
|
340
|
+
- `sails.inertia.isPrecognitive(req?)`
|
|
341
|
+
- `sails.inertia.validateOnly(req?)`
|
|
342
|
+
- `sails.inertia.shouldValidate(field, req?)`
|
|
343
|
+
- `sails.inertia.handlePrecognitionSuccess(req, res)` for custom responses
|
|
344
|
+
|
|
197
345
|
### Deferred Props
|
|
198
346
|
|
|
199
347
|
Load props after initial page render:
|
|
@@ -402,6 +550,8 @@ Copy these to `api/responses/`:
|
|
|
402
550
|
- **inertiaRedirect.js** - Handle Inertia redirects
|
|
403
551
|
- **badRequest.js** - Validation errors with redirect back
|
|
404
552
|
- **serverError.js** - Error modal in dev, graceful redirect in prod
|
|
553
|
+
- **notFound.js** - 404 status pages
|
|
554
|
+
- **forbidden.js** - 403 status pages
|
|
405
555
|
|
|
406
556
|
## Architecture
|
|
407
557
|
|
|
@@ -421,7 +571,12 @@ module.exports.inertia = {
|
|
|
421
571
|
// History encryption settings
|
|
422
572
|
history: {
|
|
423
573
|
encrypt: false
|
|
424
|
-
}
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// Production status page component.
|
|
577
|
+
// Set to false to keep classic Sails EJS error views.
|
|
578
|
+
errorPage: 'error',
|
|
579
|
+
errorStatuses: [403, 404, 500, 503]
|
|
425
580
|
}
|
|
426
581
|
```
|
|
427
582
|
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* @property {string} page - The component name to render
|
|
24
24
|
* @property {InertiaProps} [props] - Props to pass to the component
|
|
25
25
|
* @property {InertiaProps} [locals] - Additional locals for the root EJS template
|
|
26
|
+
* @property {boolean} [ssr] - Whether to server-render this response when SSR is enabled
|
|
26
27
|
*
|
|
27
28
|
* @typedef {Object} DeferOptions
|
|
28
29
|
* @property {boolean} [rescue=false] - Rescue callback failures
|
|
@@ -40,6 +41,12 @@ const inertia = require('./lib/middleware/inertia-middleware')
|
|
|
40
41
|
const render = require('./lib/render')
|
|
41
42
|
const location = require('./lib/location')
|
|
42
43
|
const requestContext = require('./lib/helpers/request-context')
|
|
44
|
+
const {
|
|
45
|
+
getValidateOnlyFields,
|
|
46
|
+
isPrecognitiveRequest,
|
|
47
|
+
sendPrecognitionSuccess,
|
|
48
|
+
shouldValidateField
|
|
49
|
+
} = require('./lib/helpers/precognition')
|
|
43
50
|
|
|
44
51
|
const DeferProp = require('./lib/props/defer-prop')
|
|
45
52
|
const OptionalProp = require('./lib/props/optional-prop')
|
|
@@ -112,11 +119,19 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
112
119
|
defaults: {
|
|
113
120
|
inertia: {
|
|
114
121
|
rootView: 'app',
|
|
122
|
+
errorPage: 'error',
|
|
123
|
+
errorStatuses: [403, 404, 500, 503],
|
|
115
124
|
// Auto-version from Shipwright manifest for cache busting
|
|
116
125
|
// Override in config/inertia.js if needed
|
|
117
126
|
version: () => getManifestVersion(),
|
|
118
127
|
history: {
|
|
119
128
|
encrypt: false
|
|
129
|
+
},
|
|
130
|
+
ssr: {
|
|
131
|
+
enabled: false,
|
|
132
|
+
bundle: '.tmp/ssr/inertia.mjs',
|
|
133
|
+
pages: false,
|
|
134
|
+
fallback: true
|
|
120
135
|
}
|
|
121
136
|
}
|
|
122
137
|
},
|
|
@@ -639,6 +654,69 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
639
654
|
return handleBadRequest(req, res, optionalData)
|
|
640
655
|
},
|
|
641
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Determine if the current request is a Precognition validation request.
|
|
659
|
+
* @param {Request} [req] - The request object. Defaults to request context.
|
|
660
|
+
* @returns {boolean} - Whether this is a Precognition request.
|
|
661
|
+
*/
|
|
662
|
+
isPrecognitive(req) {
|
|
663
|
+
return isPrecognitiveRequest(req || requestContext.getRequest())
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Get the fields requested by Precognition-Validate-Only.
|
|
668
|
+
* @param {Request} [req] - The request object. Defaults to request context.
|
|
669
|
+
* @returns {string[]} - Fields requested for validation.
|
|
670
|
+
*/
|
|
671
|
+
validateOnly(req) {
|
|
672
|
+
return getValidateOnlyFields(req || requestContext.getRequest())
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Determine if a field should run validation for the current request.
|
|
677
|
+
* For non-Precognition requests, all fields should validate.
|
|
678
|
+
* @param {string} field - Field name, with dot notation and * wildcards supported.
|
|
679
|
+
* @param {Request} [req] - The request object. Defaults to request context.
|
|
680
|
+
* @returns {boolean} - Whether to validate this field.
|
|
681
|
+
*/
|
|
682
|
+
shouldValidate(field, req) {
|
|
683
|
+
return shouldValidateField(req || requestContext.getRequest(), field)
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Handle a successful Precognition validation response.
|
|
688
|
+
* This is intended for app-level custom responses such as
|
|
689
|
+
* `api/responses/precognitionSuccess.js`.
|
|
690
|
+
* @param {Request} req - The request object
|
|
691
|
+
* @param {Response} res - The response object
|
|
692
|
+
* @returns {*} - A 204 Precognition response.
|
|
693
|
+
*/
|
|
694
|
+
handlePrecognitionSuccess(req, res) {
|
|
695
|
+
if (!res) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
'sails.inertia.handlePrecognitionSuccess() called without a response object'
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return sendPrecognitionSuccess(res)
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Send a successful Precognition validation response.
|
|
706
|
+
* Low-level alias for custom responses and non-action callers.
|
|
707
|
+
* In actions with a responseType, prefer a named exit using a
|
|
708
|
+
* `precognitionSuccess` response file.
|
|
709
|
+
* @param {Request} [req] - The request object. Defaults to request context.
|
|
710
|
+
* @param {Response} [res] - The response object. Defaults to request context.
|
|
711
|
+
* @returns {*} - A 204 Precognition response.
|
|
712
|
+
*/
|
|
713
|
+
precognitionSuccess(req, res) {
|
|
714
|
+
return this.handlePrecognitionSuccess(
|
|
715
|
+
req || requestContext.getRequest(),
|
|
716
|
+
res || requestContext.getResponse()
|
|
717
|
+
)
|
|
718
|
+
},
|
|
719
|
+
|
|
642
720
|
/**
|
|
643
721
|
* Handle server error responses for Inertia.js
|
|
644
722
|
* For Inertia requests in development, displays a styled error modal with stack trace.
|
|
@@ -653,6 +731,20 @@ module.exports = function defineInertiaHook(sails) {
|
|
|
653
731
|
return handleServerError(req, res, error)
|
|
654
732
|
},
|
|
655
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Handle application status/error pages for Inertia.js.
|
|
736
|
+
* In development, 500-level HTML responses use Youch. In production,
|
|
737
|
+
* configured Inertia apps can render an Inertia status page.
|
|
738
|
+
* @docs https://docs.sailscasts.com/boring-stack/error-handling
|
|
739
|
+
* @param {Request} req - The request object
|
|
740
|
+
* @param {Response} res - The response object
|
|
741
|
+
* @param {{ statusCode?: number, error?: ErrorLike|string|Record<string, any>|null, page?: string }} [options] - Error page options
|
|
742
|
+
* @returns {*} - Response
|
|
743
|
+
*/
|
|
744
|
+
handleErrorPage(req, res, options) {
|
|
745
|
+
return handleServerError.handleErrorPage(req, res, options)
|
|
746
|
+
},
|
|
747
|
+
|
|
656
748
|
/**
|
|
657
749
|
* Configure paginated data for infinite scrolling.
|
|
658
750
|
* Wraps Waterline paginated data with proper merge behavior and normalizes
|
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
* @typedef {import('./types').InertiaResponse} InertiaResponse
|
|
4
4
|
* @typedef {import('./types').BadRequestData} BadRequestData
|
|
5
5
|
*/
|
|
6
|
+
const { INERTIA } = require('./helpers/inertia-headers')
|
|
7
|
+
const humanizeValidationErrors = require('./helpers/humanize-validation-errors')
|
|
8
|
+
const {
|
|
9
|
+
getValidateOnlyFields,
|
|
10
|
+
isPrecognitiveRequest,
|
|
11
|
+
sendPrecognitionErrors,
|
|
12
|
+
sendPrecognitionSuccess
|
|
13
|
+
} = require('./helpers/precognition')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Record<string, string[]>} errors
|
|
17
|
+
* @param {string[]} fields
|
|
18
|
+
* @returns {Record<string, string[]>}
|
|
19
|
+
*/
|
|
20
|
+
function filterErrors(errors, fields) {
|
|
21
|
+
if (fields.length === 0) {
|
|
22
|
+
return errors
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fields.reduce((result, field) => {
|
|
26
|
+
if (errors[field]) {
|
|
27
|
+
result[field] = errors[field]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
}, /** @type {Record<string, string[]>} */ ({}))
|
|
32
|
+
}
|
|
6
33
|
|
|
7
34
|
/**
|
|
8
35
|
* Handle bad request responses for Inertia.js
|
|
@@ -24,73 +51,52 @@
|
|
|
24
51
|
*/
|
|
25
52
|
module.exports = function handleBadRequest(req, res, optionalData) {
|
|
26
53
|
const sails = req._sails
|
|
54
|
+
const log = sails.log || {}
|
|
55
|
+
const response = /** @type {any} */ (res)
|
|
27
56
|
// Define the status code to send in the response.
|
|
28
57
|
const statusCodeToSet = 400
|
|
58
|
+
const errors = humanizeValidationErrors(optionalData)
|
|
59
|
+
|
|
60
|
+
if (isPrecognitiveRequest(req)) {
|
|
61
|
+
const filteredErrors = filterErrors(errors, getValidateOnlyFields(req))
|
|
29
62
|
|
|
30
|
-
// Check if it's an Inertia request
|
|
31
|
-
if (req.header?.('X-Inertia')) {
|
|
32
63
|
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Array.isArray(optionalData.problems)
|
|
64
|
+
Object.keys(errors).length > 0 &&
|
|
65
|
+
Object.keys(filteredErrors).length === 0
|
|
36
66
|
) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
optionalData.problems.forEach((problem) => {
|
|
40
|
-
if (typeof problem === 'object') {
|
|
41
|
-
Object.keys(problem).forEach((propertyName) => {
|
|
42
|
-
const sanitizedProblem = String(problem[propertyName]).replace(
|
|
43
|
-
/\.$/,
|
|
44
|
-
''
|
|
45
|
-
) // Trim trailing dot
|
|
46
|
-
if (!errors[propertyName]) {
|
|
47
|
-
errors[propertyName] = [sanitizedProblem]
|
|
48
|
-
} else {
|
|
49
|
-
errors[propertyName].push(sanitizedProblem)
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
} else {
|
|
53
|
-
const regex = /"(.*?)"/
|
|
54
|
-
const matches = problem.match(regex)
|
|
67
|
+
return sendPrecognitionSuccess(res)
|
|
68
|
+
}
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!errors[propertyName]) {
|
|
64
|
-
errors[propertyName] = [sanitizedProblem]
|
|
65
|
-
} else {
|
|
66
|
-
errors[propertyName].push(sanitizedProblem)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
})
|
|
70
|
+
return sendPrecognitionErrors(res, filteredErrors)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if it's an Inertia request
|
|
74
|
+
if (req.header?.(INERTIA)) {
|
|
75
|
+
if (Object.keys(errors).length > 0) {
|
|
76
|
+
req.session = req.session || {}
|
|
71
77
|
req.session.errors = errors
|
|
72
|
-
return
|
|
78
|
+
return response.redirect(303, req.get('Referrer') || '/')
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// If not an Inertia request, perform the normal badRequest response
|
|
77
83
|
if (optionalData === undefined) {
|
|
78
|
-
|
|
79
|
-
return
|
|
84
|
+
log.info?.('Ran custom response: res.badRequest()')
|
|
85
|
+
return response.sendStatus(statusCodeToSet)
|
|
80
86
|
} else if (optionalData instanceof Error) {
|
|
81
|
-
|
|
87
|
+
log.info?.(
|
|
82
88
|
'Custom response `res.badRequest()` called with an Error:',
|
|
83
89
|
optionalData
|
|
84
90
|
)
|
|
85
91
|
|
|
86
92
|
if (typeof (/** @type {*} */ (optionalData).toJSON) !== 'function') {
|
|
87
93
|
if (process.env.NODE_ENV === 'production') {
|
|
88
|
-
return
|
|
94
|
+
return response.sendStatus(statusCodeToSet)
|
|
89
95
|
} else {
|
|
90
|
-
return
|
|
96
|
+
return response.status(statusCodeToSet).send(optionalData.stack)
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
} else {
|
|
94
|
-
return
|
|
100
|
+
return response.status(statusCodeToSet).send(optionalData)
|
|
95
101
|
}
|
|
96
102
|
}
|