inertia-sails 1.3.2 → 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.
Files changed (41) hide show
  1. package/README.md +55 -2
  2. package/index.js +133 -81
  3. package/lib/handle-bad-request.js +20 -6
  4. package/lib/helpers/build-page-object.js +117 -12
  5. package/lib/helpers/inertia-headers.js +4 -1
  6. package/lib/helpers/is-inertia-partial-request.js +10 -0
  7. package/lib/helpers/is-inertia-request.js +9 -0
  8. package/lib/helpers/request-context.js +49 -8
  9. package/lib/helpers/resolve-asset-version.js +12 -0
  10. package/lib/helpers/resolve-validation-errors.js +12 -5
  11. package/lib/location.js +11 -0
  12. package/lib/middleware/inertia-middleware.js +7 -2
  13. package/lib/props/always-prop.js +6 -2
  14. package/lib/props/defer-prop.js +46 -6
  15. package/lib/props/get-partial-data.js +9 -0
  16. package/lib/props/merge-prop.js +7 -4
  17. package/lib/props/merge-targets.js +114 -0
  18. package/lib/props/mergeable-prop.js +87 -0
  19. package/lib/props/once-prop.js +9 -1
  20. package/lib/props/optional-prop.js +12 -4
  21. package/lib/props/pick-props-to-resolve.js +14 -1
  22. package/lib/props/resolve-deferred-props.js +17 -1
  23. package/lib/props/resolve-except-props.js +11 -0
  24. package/lib/props/resolve-merge-props.js +81 -20
  25. package/lib/props/resolve-once-props.js +17 -7
  26. package/lib/props/resolve-only-props.js +10 -3
  27. package/lib/props/resolve-page-props.js +72 -13
  28. package/lib/props/resolve-scroll-props.js +17 -3
  29. package/lib/props/scroll-prop.js +30 -8
  30. package/lib/render.js +68 -9
  31. package/lib/responses/server-error.js +23 -6
  32. package/lib/types.js +68 -0
  33. package/package.json +3 -2
  34. package/test.js +53 -12
  35. package/tests/helpers/build-page-object.test.js +183 -0
  36. package/tests/helpers/preserve-fragment.test.js +97 -0
  37. package/tests/props/merge-targets.test.js +74 -0
  38. package/tests/props/resolve-merge-props.test.js +151 -0
  39. package/tests/props/resolve-page-props.test.js +70 -0
  40. package/tests/props/resolve-scroll-props.test.js +51 -0
  41. 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" data-page="<%- JSON.stringify(page) %>"></div>
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.js v2's `<InfiniteScroll>` component:
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('express').Request} Request
13
- * @typedef {import('express').Response} Response
14
- */
15
-
16
- /**
17
- * @typedef {Object} InertiaConfig
18
- * @property {string} [rootView='app'] - The root view template to use
19
- * @property {string|number|Function} [version=1] - Asset version for cache busting
20
- * @property {Object} [history] - History encryption settings
21
- * @property {boolean} [history.encrypt=false] - Whether to encrypt history state
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 {Object.<string, *>} [props] - Props to pass to the component
33
- * @property {Object.<string, *>} [locals] - Additional locals for the root EJS template
34
- */
35
-
36
- /**
37
- * @typedef {<T>() => T | Promise<T>} PropCallback
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.
@@ -128,16 +138,29 @@ module.exports = function defineInertiaHook(sails) {
128
138
  // the request is already wrapped in AsyncLocalStorage context.
129
139
  if (sails.config.http && sails.config.http.middleware) {
130
140
  const mw = sails.config.http.middleware
131
- if (mw.order && mw.order.indexOf('inertiaContext') === -1) {
141
+ // When order is not explicitly configured, Sails uses a default order
142
+ // internally. We need to set it here so we can inject inertiaContext
143
+ // before the router middleware.
144
+ if (!mw.order) {
145
+ mw.order = [
146
+ 'cookieParser',
147
+ 'session',
148
+ 'bodyParser',
149
+ 'compress',
150
+ 'poweredBy',
151
+ 'router',
152
+ 'www',
153
+ 'favicon'
154
+ ]
155
+ }
156
+ if (mw.order.indexOf('inertiaContext') === -1) {
132
157
  const routerIdx = mw.order.indexOf('router')
133
158
  if (routerIdx !== -1) {
134
159
  mw.order.splice(routerIdx, 0, 'inertiaContext')
135
160
  }
136
161
  }
137
162
  if (!mw.inertiaContext) {
138
- mw.inertiaContext = function inertiaContext(req, res, next) {
139
- requestContext.run(req, res, next)
140
- }
163
+ mw.inertiaContext = runWithRequestContext
141
164
  }
142
165
  }
143
166
  },
@@ -167,28 +190,12 @@ module.exports = function defineInertiaHook(sails) {
167
190
  before: {
168
191
  'GET /*': {
169
192
  skipAssets: true,
170
- fn: (req, res, next) => {
171
- // Skip if context already set up by HTTP middleware
172
- if (requestContext.getContext()) return next()
173
- requestContext.run(req, res, next)
174
- }
175
- },
176
- 'POST /*': (req, res, next) => {
177
- if (requestContext.getContext()) return next()
178
- requestContext.run(req, res, next)
193
+ fn: runWithRequestContext
179
194
  },
180
- 'PUT /*': (req, res, next) => {
181
- if (requestContext.getContext()) return next()
182
- requestContext.run(req, res, next)
183
- },
184
- 'PATCH /*': (req, res, next) => {
185
- if (requestContext.getContext()) return next()
186
- requestContext.run(req, res, next)
187
- },
188
- 'DELETE /*': (req, res, next) => {
189
- if (requestContext.getContext()) return next()
190
- requestContext.run(req, res, next)
191
- }
195
+ 'POST /*': runWithRequestContext,
196
+ 'PUT /*': runWithRequestContext,
197
+ 'PATCH /*': runWithRequestContext,
198
+ 'DELETE /*': runWithRequestContext
192
199
  }
193
200
  },
194
201
 
@@ -310,7 +317,7 @@ module.exports = function defineInertiaHook(sails) {
310
317
  * Create an optional prop
311
318
  * This allows you to define properties that are only evaluated when accessed.
312
319
  * @docs https://docs.sailscasts.com/boring-stack/partial-reloads#lazy-data-evaluation
313
- * @param {Function} callback - The callback function to execute
320
+ * @param {PropCallback} callback - The callback function to execute
314
321
  * @returns {OptionalProp} - The optional prop
315
322
  */
316
323
  optional(callback) {
@@ -321,7 +328,7 @@ module.exports = function defineInertiaHook(sails) {
321
328
  * Create a mergeable prop
322
329
  * This allows you to merge multiple props together.
323
330
  * @docs https://docs.sailscasts.com/boring-stack/merging-props
324
- * @param {Function} callback - The callback function to execute
331
+ * @param {PropCallback} callback - The callback function to execute
325
332
  * @returns {MergeProp} - The mergeable prop
326
333
  */
327
334
  merge(callback) {
@@ -332,7 +339,7 @@ module.exports = function defineInertiaHook(sails) {
332
339
  * Create an always prop
333
340
  * Always props are resolved on every request, whether partial or not.
334
341
  * @docs https://docs.sailscasts.com/boring-stack/partial-reloads#lazy-data-evaluation
335
- * @param {Function} callback - The callback function
342
+ * @param {PropCallback} callback - The callback function
336
343
  * @returns {AlwaysProp} - The always prop
337
344
  */
338
345
  always(callback) {
@@ -342,12 +349,13 @@ module.exports = function defineInertiaHook(sails) {
342
349
  * Create a deferred prop
343
350
  * This allows you to load certain page data after the initial render.
344
351
  * @docs https://docs.sailscasts.com/boring-stack/deferred-props
345
- * @param {Function} cb - The callback function to execute
346
- * @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
347
355
  * @returns {DeferProp} - The deferred prop
348
356
  */
349
- defer(cb, group = 'default') {
350
- return new DeferProp(cb, group)
357
+ defer(cb, group = 'default', options = {}) {
358
+ return new DeferProp(cb, group, options)
351
359
  },
352
360
 
353
361
  /**
@@ -356,7 +364,7 @@ module.exports = function defineInertiaHook(sails) {
356
364
  * The client tracks which props it has via X-Inertia-Except-Once-Props header.
357
365
  * Useful for expensive computations that don't change often.
358
366
  * @docs https://docs.sailscasts.com/boring-stack/once-props
359
- * @param {Function} callback - The callback function to execute
367
+ * @param {PropCallback} callback - The callback function to execute
360
368
  * @returns {OnceProp} - The once prop
361
369
  * @example
362
370
  * // Basic usage
@@ -379,7 +387,7 @@ module.exports = function defineInertiaHook(sails) {
379
387
  * Combines share() and once() - the prop is shared and only resolved once.
380
388
  * @docs https://docs.sailscasts.com/boring-stack/once-props#share-once
381
389
  * @param {string} key - The key of the property
382
- * @param {Function} callback - The callback function to execute
390
+ * @param {PropCallback} callback - The callback function to execute
383
391
  * @returns {OnceProp} - The once prop (for chaining)
384
392
  * @example
385
393
  * // In a policy or middleware
@@ -430,7 +438,7 @@ module.exports = function defineInertiaHook(sails) {
430
438
  * This prevents "phantom" toasts/notifications when users navigate back.
431
439
  * Flash data is stored in the session so it persists across redirects.
432
440
  * @docs https://docs.sailscasts.com/boring-stack/flash
433
- * @param {string|Object} key - The key or an object of key-value pairs
441
+ * @param {string|Record<string, any>} key - The key or an object of key-value pairs
434
442
  * @param {*} [value] - The value (if key is a string)
435
443
  * @returns {Object} - The hook instance for chaining
436
444
  * @example
@@ -454,7 +462,8 @@ module.exports = function defineInertiaHook(sails) {
454
462
  if (typeof key === 'object' && key !== null) {
455
463
  req.session._inertiaFlash = { ...req.session._inertiaFlash, ...key }
456
464
  } else {
457
- req.session._inertiaFlash[key] = value
465
+ const flashKey = /** @type {string} */ (key)
466
+ req.session._inertiaFlash[flashKey] = value
458
467
  }
459
468
  return this
460
469
  },
@@ -471,8 +480,8 @@ module.exports = function defineInertiaHook(sails) {
471
480
  /**
472
481
  * Consume and clear flash data from the session.
473
482
  * Called internally by build-page-object after adding to response.
474
- * @param {Object} req - The request object
475
- * @returns {Object} - The flash data that was consumed
483
+ * @param {Request} req - The request object
484
+ * @returns {InertiaProps} - The flash data that was consumed
476
485
  */
477
486
  consumeFlash(req) {
478
487
  const flash = req?.session?._inertiaFlash || {}
@@ -486,7 +495,7 @@ module.exports = function defineInertiaHook(sails) {
486
495
  * Create a deep merge prop
487
496
  * Like merge(), but recursively merges nested objects instead of replacing them.
488
497
  * @docs https://docs.sailscasts.com/boring-stack/merging-props#deep-merge
489
- * @param {Function} callback - The callback function to execute
498
+ * @param {PropCallback} callback - The callback function to execute
490
499
  * @returns {MergeProp} - The mergeable prop with deep merge enabled
491
500
  * @example
492
501
  * // Deep merge nested user preferences
@@ -500,9 +509,9 @@ module.exports = function defineInertiaHook(sails) {
500
509
 
501
510
  /**
502
511
  * Render the response
503
- * @param {Object} req - The request object
504
- * @param {Object} res - The response object
505
- * @param {Object} data - The data to render
512
+ * @param {Request} req - The request object
513
+ * @param {Response} res - The response object
514
+ * @param {InertiaRenderData} data - The data to render
506
515
  * @returns {*} - The rendered response
507
516
  */
508
517
  render(req, res, data) {
@@ -512,8 +521,8 @@ module.exports = function defineInertiaHook(sails) {
512
521
  * Handle Inertia redirects (external URLs or non-Inertia pages)
513
522
  * Forces a full page visit instead of an Inertia XHR request.
514
523
  * See https://docs.sailscasts.com/boring-stack/redirects
515
- * @param {Object} req - The request object
516
- * @param {Object} res - The response object
524
+ * @param {Request} req - The request object
525
+ * @param {Response} res - The response object
517
526
  * @param {string} url - The URL to redirect to
518
527
  * @returns {Object} - The response object with the redirect
519
528
  */
@@ -570,12 +579,60 @@ module.exports = function defineInertiaHook(sails) {
570
579
  return requestContext.getClearHistory()
571
580
  },
572
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
+
573
630
  /**
574
631
  * Handle bad request responses for Inertia.js
575
632
  * For Inertia requests with validation errors, redirects back with errors in session.
576
- * @param {Object} req - The request object
577
- * @param {Object} res - The response object
578
- * @param {Object|Error} [optionalData] - Optional error data or Error object
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
579
636
  * @returns {*} - Response (redirect for Inertia, status code for non-Inertia)
580
637
  */
581
638
  handleBadRequest(req, res, optionalData) {
@@ -587,9 +644,9 @@ module.exports = function defineInertiaHook(sails) {
587
644
  * For Inertia requests in development, displays a styled error modal with stack trace.
588
645
  * In production, redirects back with a flash error message.
589
646
  * @docs https://docs.sailscasts.com/boring-stack/error-handling
590
- * @param {Object} req - The request object
591
- * @param {Object} res - The response object
592
- * @param {Object|Error} [error] - Optional error data or Error object
647
+ * @param {Request} req - The request object
648
+ * @param {Response} res - The response object
649
+ * @param {ErrorLike} [error] - Optional error data or Error object
593
650
  * @returns {*} - Response (HTML modal for dev Inertia, redirect for prod)
594
651
  */
595
652
  handleServerError(req, res, error) {
@@ -605,13 +662,8 @@ module.exports = function defineInertiaHook(sails) {
605
662
  * to 1-based for the Inertia client.
606
663
  *
607
664
  * @docs https://docs.sailscasts.com/boring-stack/infinite-scroll
608
- * @param {Function} callback - Callback returning the paginated data array
609
- * @param {Object} [options] - Pagination options
610
- * @param {number} [options.page=0] - Current page index (0-based)
611
- * @param {number} [options.perPage=10] - Items per page
612
- * @param {number} [options.total=0] - Total number of items
613
- * @param {string} [options.pageName='page'] - Query parameter name for pagination
614
- * @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
615
667
  * @returns {ScrollProp} - The scroll prop
616
668
  * @example
617
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 {Object} req - Express/Sails request object
9
- * @param {Object} res - Express/Sails response object
10
- * @param {Object|Error} [optionalData] - Optional error data or Error object
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 (optionalData && optionalData.problems) {
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(/\.$/, '') // Trim trailing dot
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 {