-
-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathprops.ts
More file actions
655 lines (612 loc) · 18.1 KB
/
props.ts
File metadata and controls
655 lines (612 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
/*
* @adonisjs/inertia
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { BaseSerializer, Collection, Item, Paginator } from '@adonisjs/core/transformers'
import { type AsyncOrSync } from '@adonisjs/core/types/common'
import { type JSONDataTypes } from '@adonisjs/core/types/transformers'
import { ALWAYS_PROP, DEEP_MERGE, DEFERRED_PROP, OPTIONAL_PROP, TO_BE_MERGED } from './symbols.ts'
import {
type DeferProp,
type PageProps,
type AlwaysProp,
type OptionalProp,
type MergeableProp,
type ComponentProps,
type UnPackedPageProps,
} from './types.ts'
import { type ContainerResolver } from '@adonisjs/core/container'
class InertiaSerializer extends BaseSerializer {
wrap: undefined = undefined
definePaginationMetaData(metaData: unknown): unknown {
return metaData
}
}
const inertiaSerializer = new InertiaSerializer()
/**
* Type guard to check if a value is a plain object
*
* @param value - The value to check
* @returns True if the value is a plain object (not null, not array)
*/
function isObject(value: unknown): value is Record<PropertyKey, any> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
/**
* Creates a deferred prop that is never included in standard visits but must be shared with
* the client during standard visits. Can be explicitly requested and supports merging.
*
* Deferred props are useful for expensive computations that should only be loaded when
* specifically requested by the client.
*
* @param fn - Function that computes the prop value when requested
* @returns A deferred prop object with compute and merge capabilities
*
* @example
* ```javascript
* // Create a deferred prop for expensive user statistics
* const userStats = defer(() => {
* return calculateExpensiveUserStats(userId)
* })
*
* // Use in page props
* return inertia.render('dashboard', {
* user: user,
* stats: userStats // Only loaded when explicitly requested
* })
* ```
*/
export function defer<T extends UnPackedPageProps>(
fn: () => AsyncOrSync<T>,
group: string = 'default'
): DeferProp<T> {
return {
group,
compute: fn,
merge() {
return merge(this)
},
[DEFERRED_PROP]: true,
}
}
/**
* Creates an optional prop that is never included in standard visits and can only be
* explicitly requested by the client. Unlike deferred props, optional props are not
* shared with the client during standard visits.
*
* Optional props are ideal for data that is rarely needed and should only be loaded
* on demand to optimize performance.
*
* @param fn - Function that computes the prop value when requested
* @returns An optional prop object that computes values lazily
*
* @example
* ```javascript
* // Create an optional prop for detailed audit logs
* const auditLogs = optional(() => {
* return fetchDetailedAuditLogs(resourceId)
* })
*
* // Use in page props
* return inertia.render('resource/show', {
* resource: resource,
* auditLogs: auditLogs // Only loaded when explicitly requested
* })
* ```
*/
export function optional<T extends UnPackedPageProps>(fn: () => AsyncOrSync<T>): OptionalProp<T> {
return {
compute: fn,
[OPTIONAL_PROP]: true,
}
}
/**
* Creates a prop that is always included in responses and cannot be removed during
* cherry-picking. This ensures the prop is always available to the frontend component.
*
* Always props are useful for critical data that the frontend component must have
* to function properly, regardless of what props are specifically requested.
*
* @param value - The value to always include in the response
* @returns An always prop object that cannot be cherry-picked away
*
* @example
* ```javascript
* // Create an always prop for critical user permissions
* const userPermissions = always(user.permissions)
*
* // Use in page props
* return inertia.render('admin/dashboard', {
* users: users,
* permissions: userPermissions // Always included, never filtered out
* })
* ```
*/
export function always<T extends UnPackedPageProps>(value: T): AlwaysProp<T> {
return {
value,
[ALWAYS_PROP]: true,
}
}
/**
* Creates a prop that should be merged with existing props on the page rather than
* replaced. This is useful for incremental updates where you want to combine new
* data with existing data on the client side.
*
* Mergeable props enable efficient partial updates by allowing the client to
* merge new prop values with existing ones instead of replacing them entirely.
*
* @param value - The value to be merged with existing props
* @returns A mergeable prop object marked for merging behavior
*
* @example
* ```javascript
* // Create a mergeable prop for incremental notifications
* const newNotifications = merge([
* { id: 1, message: 'New message received' },
* { id: 2, message: 'Task completed' }
* ])
*
* // Use in page props - will merge with existing notifications
* return inertia.render('dashboard', {
* user: user,
* notifications: newNotifications // Merges with existing notifications array
* })
* ```
*/
export function merge<T extends UnPackedPageProps | DeferProp<UnPackedPageProps>>(
value: T
): MergeableProp<T> {
return {
value,
[TO_BE_MERGED]: true,
[DEEP_MERGE]: false,
}
}
/**
* Creates a prop that should be deeply merged with existing props on the page.
*
* Unlike shallow merge, deep merge recursively merges nested objects and arrays,
* allowing for more granular updates to complex data structures.
*
* @param value - The value to be deeply merged with existing props
* @returns A mergeable prop object marked for deep merging behavior
*
* @example
* ```javascript
* // Create a deep mergeable prop for nested user settings
* const updatedSettings = deepMerge({
* notifications: {
* email: true,
* push: false
* },
* privacy: {
* profile: 'public'
* }
* })
*
* // Use in page props - will deeply merge with existing settings
* return inertia.render('settings', {
* user: user,
* settings: updatedSettings // Deep merges with existing settings object
* })
* ```
*/
export function deepMerge<T extends UnPackedPageProps | DeferProp<UnPackedPageProps>>(
value: T
): MergeableProp<T> {
return {
value,
[TO_BE_MERGED]: true,
[DEEP_MERGE]: true,
}
}
/**
* Type guard that checks if a prop value is a deferred prop.
*
* Deferred props contain the DEFERRED_PROP symbol and have compute/merge capabilities.
* This function is useful for runtime type checking and conditional prop handling.
*
* @param propValue - The object to check for deferred prop characteristics
* @returns True if the prop value is a deferred prop
*
* @example
* ```js
* const prop = defer(() => ({ data: 'value' }))
*
* if (isDeferredProp(prop)) {
* // prop is now typed as DeferProp<T>
* const result = prop.compute()
* }
* ```
*/
export function isDeferredProp<T extends UnPackedPageProps>(
propValue: Object
): propValue is DeferProp<T> {
return DEFERRED_PROP in propValue
}
/**
* Type guard that checks if a prop value is a mergeable prop.
*
* Mergeable props contain the TO_BE_MERGED symbol and should be merged with
* existing props rather than replaced during updates.
*
* @param propValue - The object to check for mergeable prop characteristics
* @returns True if the prop value is a mergeable prop
*
* @example
* ```js
* const prop = merge({ items: [1, 2, 3] })
*
* if (isMergeableProp(prop)) {
* // prop is now typed as MergeableProp<T>
* const value = prop.value
* }
* ```
*/
export function isMergeableProp<T extends UnPackedPageProps | DeferProp<UnPackedPageProps>>(
propValue: Object
): propValue is MergeableProp<T> {
return TO_BE_MERGED in propValue
}
/**
* Type guard that checks if a prop value is an always prop.
*
* Always props contain the ALWAYS_PROP symbol and are always included in
* responses, regardless of cherry-picking or selective prop requests.
*
* @param propValue - The object to check for always prop characteristics
* @returns True if the prop value is an always prop
*
* @example
* ```js
* const prop = always({ userId: 123, permissions: ['read', 'write'] })
*
* if (isAlwaysProp(prop)) {
* // prop is now typed as AlwaysProp<T>
* const value = prop.value
* }
* ```
*/
export function isAlwaysProp<T extends UnPackedPageProps>(
propValue: Object
): propValue is AlwaysProp<T> {
return ALWAYS_PROP in propValue
}
/**
* Type guard that checks if a prop value is an optional prop.
*
* Optional props contain the OPTIONAL_PROP symbol and are only included
* when explicitly requested by the client, never in standard visits.
*
* @param propValue - The object to check for optional prop characteristics
* @returns True if the prop value is an optional prop
*
* @example
* ```js
* const prop = optional(() => ({ detailedData: 'expensive computation' }))
*
* if (isOptionalProp(prop)) {
* // prop is now typed as OptionalProp<T>
* const result = prop.compute()
* }
* ```
*/
export function isOptionalProp<T extends UnPackedPageProps>(
propValue: Object
): propValue is OptionalProp<T> {
return OPTIONAL_PROP in propValue
}
/**
* Checks if a value is a transformer instance (Item, Collection, or Paginator).
*/
function isTransformerInstance(value: unknown): boolean {
return value instanceof Item || value instanceof Collection || value instanceof Paginator
}
/**
* Helper function to unpack prop values using the transformer serialize function.
*
* @param value - The prop value to serialize
* @param containerResolver - Container resolver for dependency injection
* @returns Promise resolving to the serialized JSON data
*/
async function unpackPropValue(
value: UnPackedPageProps<JSONDataTypes>,
containerResolver: ContainerResolver<any>
): Promise<JSONDataTypes> {
if (isTransformerInstance(value)) {
return inertiaSerializer.serialize(value, containerResolver) as Promise<JSONDataTypes>
}
if (Array.isArray(value)) {
return Promise.all(value.map((item) => unpackPropValue(item, containerResolver)))
}
if (isObject(value)) {
return inertiaSerializer.serialize(value, containerResolver) as Promise<JSONDataTypes>
}
return value
}
/**
* Builds props for standard (non-partial) Inertia visits.
*
* This function processes page props and categorizes them based on their type:
* - Deferred props: Skipped but communicated to client
* - Optional props: Skipped entirely
* - Always props: Always included
* - Mergeable props: Included and marked for merging
* - Regular props: Included normally
*
* @param pageProps - The page props to process
* @param containerResolver - Container resolver for dependency injection
* @returns Promise resolving to object containing processed props, deferred props list, and merge props list
*
* @example
* ```js
* const result = await buildStandardVisitProps({
* user: { name: 'John' },
* posts: defer(() => getPosts()),
* settings: merge({ theme: 'dark' })
* })
* // Returns: { props: { user: {...} }, deferredProps: { default: ['posts'] }, mergeProps: ['settings'] }
* ```
*/
export async function buildStandardVisitProps(
pageProps: PageProps,
containerResolver: ContainerResolver<any>
) {
const mergeProps: string[] = []
const deepMergeProps: string[] = []
const newProps: ComponentProps = {}
const deferredProps: { [group: string]: string[] } = {}
const unpackedValues: Array<{
key: string
value: UnPackedPageProps | (() => AsyncOrSync<UnPackedPageProps>)
}> = []
for (const [key, value] of Object.entries(pageProps)) {
if (isObject(value)) {
/**
* Deferred props are skipped during the standard visits.
* But we inform the client about it
*/
if (isDeferredProp(value)) {
deferredProps[value.group] = deferredProps[value.group] ?? []
deferredProps[value.group].push(key)
continue
}
/**
* Optional props are skipped during the standard visits
*/
if (isOptionalProp(value)) {
continue
}
/**
* Unpack always prop value
*/
if (isAlwaysProp(value)) {
unpackedValues.push({ key, value: value.value })
continue
}
/**
* Inform the client about the mergeable prop and use its
* value
*/
if (isMergeableProp(value)) {
if (value[DEEP_MERGE]) {
deepMergeProps.push(key)
} else {
mergeProps.push(key)
}
/**
* Mergeable deferred props are skipped during the standard visits.
* But we inform the client about both of them
*/
if (isObject(value.value) && isDeferredProp(value.value)) {
deferredProps[value.value.group] = deferredProps[value.value.group] ?? []
deferredProps[value.value.group].push(key)
unpackedValues.push({
key,
value: value.value.compute,
})
} else {
unpackedValues.push({
key,
value: value.value,
})
}
continue
}
/**
* Unpack all other values
*/
unpackedValues.push({
key,
value: value,
})
} else if (Array.isArray(value)) {
/**
* Arrays may contain nested transformer instances
* that need to be resolved
*/
unpackedValues.push({
key,
value: value,
})
} else {
/**
* Compute lazy value
*/
if (typeof value === 'function') {
unpackedValues.push({
key,
value: value,
})
continue
}
newProps[key] = value
}
}
await Promise.all(
unpackedValues.map(async ({ key, value }) => {
if (typeof value === 'function') {
return Promise.resolve(value())
.then((r) => unpackPropValue(r, containerResolver))
.then((jsonValue) => {
newProps[key] = jsonValue
})
} else {
return unpackPropValue(value, containerResolver).then((jsonValue) => {
newProps[key] = jsonValue
})
}
})
)
return {
props: newProps,
mergeProps,
deepMergeProps,
deferredProps,
}
}
/**
* Builds props for partial (cherry-picked) Inertia requests.
*
* This function processes page props for partial requests where only specific
* props are requested. It handles:
* - Always props: Always included regardless of cherry picking
* - Cherry-picked props: Only included if in the cherryPickProps list
* - Mergeable props: Included and marked for merging
* - Regular props: Included if cherry-picked
*
* @param pageProps - The page props to process
* @param cherryPickProps - Array of prop names to include
* @param containerResolver - Container resolver for dependency injection
* @returns Promise resolving to object containing processed props and merge props list
*
* @example
* ```js
* const result = await buildPartialRequestProps(
* { user: { name: 'John' }, posts: defer(() => getPosts()), stats: optional(() => getStats()) },
* ['posts', 'stats']
* )
* // Returns: { props: { posts: [...], stats: [...] }, mergeProps: [], deferredProps: {} }
* ```
*/
export async function buildPartialRequestProps(
pageProps: PageProps,
cherryPickProps: string[],
containerResolver: ContainerResolver<any>
) {
const mergeProps: string[] = []
const deepMergeProps: string[] = []
const newProps: ComponentProps = {}
const unpackedValues: Array<{
key: string
value: UnPackedPageProps | (() => AsyncOrSync<UnPackedPageProps>)
}> = []
for (const [key, value] of Object.entries(pageProps)) {
if (isObject(value)) {
/**
* Unpack always prop even if it is not part of
* cherry picking props
*/
if (isAlwaysProp(value)) {
unpackedValues.push({ key, value: value.value })
continue
}
/**
* Skip key if not part of cherry picking list
*/
if (!cherryPickProps.includes(key)) {
continue
}
/**
* Unpack deferred prop
*/
if (isDeferredProp(value)) {
unpackedValues.push({ key, value: value.compute })
continue
}
/**
* Unpack optional prop
*/
if (isOptionalProp(value)) {
unpackedValues.push({ key, value: value.compute })
continue
}
/**
* Inform the client about the mergeable prop
*/
if (isMergeableProp(value)) {
if (value[DEEP_MERGE]) {
deepMergeProps.push(key)
} else {
mergeProps.push(key)
}
/**
* Unpack deferred mergeable prop
*/
if (isObject(value.value) && isDeferredProp(value.value)) {
unpackedValues.push({ key, value: value.value.compute })
} else {
unpackedValues.push({ key, value: value.value })
}
continue
}
/**
* Unpack all other values
*/
unpackedValues.push({ key, value: value as UnPackedPageProps })
} else if (Array.isArray(value)) {
/**
* Skip key if not part of cherry picking list
*/
if (!cherryPickProps.includes(key)) {
continue
}
/**
* Arrays may contain nested transformer instances
* that need to be resolved
*/
unpackedValues.push({ key, value: value as UnPackedPageProps })
} else {
/**
* Skip key if not part of cherry picking list
*/
if (!cherryPickProps.includes(key)) {
continue
}
/**
* Compute lazy value
*/
if (typeof value === 'function') {
unpackedValues.push({ key, value })
continue
}
newProps[key] = value
}
}
await Promise.all(
unpackedValues.map(async ({ key, value }) => {
if (typeof value === 'function') {
return Promise.resolve(value())
.then((r) => unpackPropValue(r, containerResolver))
.then((jsonValue) => {
newProps[key] = jsonValue
})
} else {
return unpackPropValue(value, containerResolver).then((jsonValue) => {
newProps[key] = jsonValue
})
}
})
)
return {
props: newProps,
mergeProps,
deepMergeProps,
deferredProps: {},
}
}