@scalar/json-magic 0.1.0 → 0.3.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 (57) hide show
  1. package/.turbo/turbo-build.log +4 -3
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +21 -3
  4. package/dist/bundle/bundle.d.ts +84 -14
  5. package/dist/bundle/bundle.d.ts.map +1 -1
  6. package/dist/bundle/bundle.js +56 -15
  7. package/dist/bundle/bundle.js.map +3 -3
  8. package/dist/bundle/index.d.ts +2 -1
  9. package/dist/bundle/index.d.ts.map +1 -1
  10. package/dist/bundle/index.js.map +2 -2
  11. package/dist/bundle/plugins/fetch-urls/index.d.ts +2 -2
  12. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  13. package/dist/bundle/plugins/fetch-urls/index.js +1 -0
  14. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  15. package/dist/bundle/plugins/parse-json/index.d.ts +2 -2
  16. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  17. package/dist/bundle/plugins/parse-json/index.js +1 -0
  18. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  19. package/dist/bundle/plugins/parse-yaml/index.d.ts +2 -2
  20. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  21. package/dist/bundle/plugins/parse-yaml/index.js +1 -0
  22. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  23. package/dist/bundle/plugins/read-files/index.d.ts +2 -2
  24. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  25. package/dist/bundle/plugins/read-files/index.js +1 -0
  26. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  27. package/dist/diff/apply.d.ts +1 -1
  28. package/dist/diff/apply.d.ts.map +1 -1
  29. package/dist/diff/apply.js.map +2 -2
  30. package/dist/diff/diff.d.ts +2 -2
  31. package/dist/diff/diff.d.ts.map +1 -1
  32. package/dist/diff/diff.js.map +2 -2
  33. package/dist/diff/merge.d.ts +3 -3
  34. package/dist/diff/merge.d.ts.map +1 -1
  35. package/dist/diff/merge.js.map +2 -2
  36. package/dist/magic-proxy/proxy.d.ts +23 -42
  37. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  38. package/dist/magic-proxy/proxy.js +103 -80
  39. package/dist/magic-proxy/proxy.js.map +3 -3
  40. package/dist/utils/is-object.d.ts +1 -1
  41. package/dist/utils/is-object.d.ts.map +1 -1
  42. package/dist/utils/is-object.js.map +2 -2
  43. package/package.json +11 -10
  44. package/src/bundle/bundle.test.ts +594 -44
  45. package/src/bundle/bundle.ts +167 -32
  46. package/src/bundle/index.ts +2 -1
  47. package/src/bundle/plugins/fetch-urls/index.ts +3 -2
  48. package/src/bundle/plugins/parse-json/index.ts +3 -2
  49. package/src/bundle/plugins/parse-yaml/index.ts +3 -2
  50. package/src/bundle/plugins/read-files/index.ts +4 -2
  51. package/src/dereference/dereference.test.ts +26 -18
  52. package/src/diff/apply.ts +8 -3
  53. package/src/diff/diff.ts +3 -3
  54. package/src/diff/merge.ts +6 -6
  55. package/src/magic-proxy/proxy.test.ts +1095 -100
  56. package/src/magic-proxy/proxy.ts +150 -171
  57. package/src/utils/is-object.ts +1 -1
@@ -1,145 +1,1140 @@
1
- import { createMagicProxy } from './proxy'
1
+ import { createMagicProxy, getRaw } from './proxy'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
4
  describe('createMagicProxy', () => {
5
- it('should correctly proxy internal refs', () => {
6
- const input = {
7
- a: 'hello',
8
- b: {
9
- '$ref': '#/a',
10
- },
11
- }
5
+ describe('get', () => {
6
+ it('should correctly proxy internal refs', () => {
7
+ const input = {
8
+ a: 'hello',
9
+ b: {
10
+ '$ref': '#/a',
11
+ },
12
+ }
12
13
 
13
- const result = createMagicProxy(input)
14
+ const result = createMagicProxy(input)
15
+ expect(result.b).toEqual({ $ref: '#/a', '$ref-value': 'hello' })
16
+ })
14
17
 
15
- expect(result.b).toBe('hello')
16
- })
18
+ it('should correctly proxy deep nested refs', () => {
19
+ const input = {
20
+ a: {
21
+ b: {
22
+ c: {
23
+ d: {
24
+ prop: 'hello',
25
+ },
26
+ e: {
27
+ '$ref': '#/a/b/c/d',
28
+ },
29
+ },
30
+ },
31
+ },
32
+ }
17
33
 
18
- it('should correctly proxy deep nested refs', () => {
19
- const input = {
20
- a: {
21
- b: {
22
- c: {
23
- d: {
34
+ const result = createMagicProxy(input) as any
35
+ expect(result.a.b.c.e['$ref-value'].prop).toBe('hello')
36
+ })
37
+
38
+ it('should correctly proxy multi refs', () => {
39
+ const input = {
40
+ a: {
41
+ b: {
42
+ c: {
43
+ prop: 'hello',
44
+ },
45
+ },
46
+ },
47
+ e: {
48
+ f: {
49
+ $ref: '#/a/b/c/prop',
50
+ },
51
+ },
52
+ d: {
53
+ $ref: '#/e/f',
54
+ },
55
+ }
56
+
57
+ const result = createMagicProxy(input)
58
+
59
+ expect(result.d).toEqual({
60
+ $ref: '#/e/f',
61
+ '$ref-value': {
62
+ $ref: '#/a/b/c/prop',
63
+ '$ref-value': 'hello',
64
+ },
65
+ })
66
+ })
67
+
68
+ it('should preserve information about the ref when the ref is resolved', () => {
69
+ const input = {
70
+ a: {
71
+ b: {
72
+ c: {
73
+ d: {
74
+ prop: 'hello',
75
+ },
76
+ e: {
77
+ '$ref': '#/a/b/c/d',
78
+ },
79
+ },
80
+ },
81
+ },
82
+ }
83
+
84
+ const result = createMagicProxy(input)
85
+ expect(result.a.b.c.e).toEqual({
86
+ '$ref': '#/a/b/c/d',
87
+ '$ref-value': {
88
+ prop: 'hello',
89
+ },
90
+ })
91
+ })
92
+
93
+ it('should resolve the references inside an array', () => {
94
+ const input = {
95
+ $defs: {
96
+ a: {
97
+ b: {
24
98
  prop: 'hello',
25
99
  },
26
- e: {
27
- '$ref': '#/a/b/c/d',
100
+ c: {
101
+ someOtherProp: 'world',
28
102
  },
29
103
  },
30
104
  },
31
- },
32
- }
105
+ a: {
106
+ b: [
107
+ {
108
+ $ref: '#/$defs/a/b',
109
+ },
110
+ {
111
+ $ref: '#/$defs/a/c',
112
+ },
113
+ ],
114
+ },
115
+ }
116
+
117
+ const result = createMagicProxy(input)
118
+ expect(JSON.stringify(result.a)).toEqual(
119
+ JSON.stringify({
120
+ 'b': [
121
+ { '$ref': '#/$defs/a/b', '$ref-value': { 'prop': 'hello' } },
122
+ { '$ref': '#/$defs/a/c', '$ref-value': { 'someOtherProp': 'world' } },
123
+ ],
124
+ }),
125
+ )
126
+ })
33
127
 
34
- const result = createMagicProxy(input) as any
35
- expect(result.a.b.c.e.prop).toBe('hello')
128
+ it('should return undefined if the ref is an external ref', () => {
129
+ const input = {
130
+ a: 'hello',
131
+ b: {
132
+ $ref: 'https://example.com/document.json/#',
133
+ },
134
+ }
135
+
136
+ const result = createMagicProxy(input)
137
+ expect(result).toEqual({
138
+ a: 'hello',
139
+ b: {
140
+ $ref: 'https://example.com/document.json/#',
141
+ '$ref-value': undefined,
142
+ },
143
+ })
144
+ })
36
145
  })
37
146
 
38
- it('should correctly proxy multi refs', () => {
39
- const input = {
40
- a: {
147
+ describe('set', () => {
148
+ it('sets properties on the target object', () => {
149
+ const input: any = {
150
+ a: 'hello',
41
151
  b: {
42
- c: {
43
- prop: 'hello',
152
+ $ref: '#/a',
153
+ },
154
+ }
155
+
156
+ const result = createMagicProxy(input)
157
+ result.c = 'world'
158
+
159
+ expect(result.c).toBe('world')
160
+ })
161
+
162
+ it('sets properties on nested objects', () => {
163
+ const input: any = {
164
+ a: {
165
+ b: {
166
+ c: 'hello',
167
+ },
168
+ },
169
+ }
170
+
171
+ const result = createMagicProxy(input)
172
+ result.a.b.d = 'world'
173
+
174
+ expect(result.a.b.d).toBe('world')
175
+ })
176
+
177
+ it('sets properties on objects with refs', () => {
178
+ const input: any = {
179
+ a: {
180
+ b: {
181
+ c: 'hello',
182
+ },
183
+ },
184
+ d: {
185
+ $ref: '#/a/b',
186
+ },
187
+ }
188
+
189
+ const result = createMagicProxy(input)
190
+ result.d.e = 'world'
191
+
192
+ expect(result.d.e).toBe('world')
193
+ })
194
+
195
+ it('sets properties on arrays', () => {
196
+ const input = {
197
+ items: [{ name: 'item1' }, { name: 'item2' }],
198
+ }
199
+
200
+ const result = createMagicProxy(input)
201
+ result.items[2] = { name: 'item3' }
202
+
203
+ expect(result.items[2].name).toBe('item3')
204
+ })
205
+
206
+ it('sets properties on array elements with refs', () => {
207
+ const input: any = {
208
+ $defs: {
209
+ item: { name: 'default' },
210
+ },
211
+ items: [{ $ref: '#/$defs/item' }],
212
+ }
213
+
214
+ const result = createMagicProxy(input)
215
+ result.items[0].id = 123
216
+
217
+ expect(input.items[0].id).toBe(123)
218
+ })
219
+
220
+ it('overwrites existing properties', () => {
221
+ const input = {
222
+ a: 'hello',
223
+ b: {
224
+ c: 'world',
225
+ },
226
+ }
227
+
228
+ const result = createMagicProxy(input)
229
+ result.a = 'updated'
230
+ result.b.c = 'updated'
231
+
232
+ expect(result.a).toBe('updated')
233
+ expect(result.b.c).toBe('updated')
234
+ })
235
+
236
+ it('sets properties on the root object', () => {
237
+ const input: any = {
238
+ a: 'hello',
239
+ }
240
+
241
+ const result = createMagicProxy(input)
242
+ result.rootProperty = 'root value'
243
+
244
+ expect(result.rootProperty).toBe('root value')
245
+ })
246
+
247
+ it('sets properties that are accessed via ref-value', () => {
248
+ const input: any = {
249
+ a: {
250
+ b: {
251
+ c: 'hello',
252
+ },
253
+ },
254
+ d: {
255
+ $ref: '#/a/b',
256
+ },
257
+ }
258
+
259
+ const result = createMagicProxy(input)
260
+ const refValue = result.d['$ref-value']
261
+ refValue.newProp = 'new value'
262
+
263
+ expect(refValue.newProp).toBe('new value')
264
+ expect(input.a.b.newProp).toBe('new value')
265
+ })
266
+
267
+ it('sets properties on deeply nested refs', () => {
268
+ const input: any = {
269
+ a: {
270
+ b: {
271
+ c: {
272
+ d: {
273
+ e: 'hello',
274
+ },
275
+ },
44
276
  },
45
277
  },
46
- },
47
- e: {
48
278
  f: {
49
- $ref: '#/a/b/c/prop',
279
+ $ref: '#/a/b/c/d',
50
280
  },
51
- },
52
- d: {
53
- $ref: '#/e/f',
54
- },
55
- }
281
+ }
56
282
 
57
- const result = createMagicProxy(input)
283
+ const result = createMagicProxy(input)
284
+ result.f['$ref-value'].g = 'world'
58
285
 
59
- expect(result.d).toBe('hello')
60
- })
286
+ expect(result.f['$ref-value'].g).toBe('world')
287
+ expect(input.a.b.c.d.g).toBe('world')
288
+ })
289
+
290
+ it('correctly writes on the referenced value', () => {
291
+ const input = {
292
+ a: {
293
+ b: {
294
+ hello: 'hi',
295
+ },
296
+ },
297
+ c: {
298
+ $ref: '#/a/b',
299
+ },
300
+ }
301
+
302
+ const proxied = createMagicProxy(input)
303
+
304
+ proxied.c['$ref-value'].hello = 'new value'
305
+
306
+ expect(input).toEqual({
307
+ a: {
308
+ b: {
309
+ hello: 'new value',
310
+ },
311
+ },
312
+ c: {
313
+ $ref: '#/a/b',
314
+ },
315
+ })
316
+ })
317
+
318
+ it('sets properties on the referenced value #1', () => {
319
+ const input = {
320
+ a: {
321
+ $ref: '#/b',
322
+ },
323
+ b: {
324
+ hello: 'world',
325
+ },
326
+ }
327
+
328
+ const proxied = createMagicProxy(input)
329
+
330
+ proxied.a['$ref-value'] = 'new value'
331
+
332
+ expect(proxied).toEqual({
333
+ a: {
334
+ $ref: '#/b',
335
+ '$ref-value': 'new value',
336
+ },
337
+ b: 'new value',
338
+ })
339
+ })
61
340
 
62
- it('should preserve information about the ref when the ref is resolved', () => {
63
- const input = {
64
- a: {
341
+ it('sets properties on the referenced value #2', () => {
342
+ const input = {
343
+ a: {
344
+ $ref: '#/b/c',
345
+ },
65
346
  b: {
66
347
  c: {
67
- d: {
68
- prop: 'hello',
69
- },
70
- e: {
71
- '$ref': '#/a/b/c/d',
72
- },
348
+ hello: 'world',
73
349
  },
74
350
  },
75
- },
76
- }
351
+ }
352
+
353
+ const proxied = createMagicProxy(input)
354
+
355
+ proxied.a['$ref-value'] = {
356
+ message: 'this is the new value',
357
+ }
77
358
 
78
- const result = createMagicProxy(input)
79
- expect(result.a.b.c.e).toEqual({
80
- prop: 'hello',
81
- 'x-original-ref': '#/a/b/c/d',
359
+ expect(proxied).toEqual({
360
+ a: {
361
+ $ref: '#/b/c',
362
+ '$ref-value': {
363
+ 'message': 'this is the new value',
364
+ },
365
+ },
366
+ 'b': {
367
+ 'c': {
368
+ 'message': 'this is the new value',
369
+ },
370
+ },
371
+ })
82
372
  })
83
- })
84
373
 
85
- it('should preserve the first ref on nested refs', () => {
86
- const input = {
87
- a: {
374
+ it('throws an error when trying set rewrite a referenced value which is the root of the document', () => {
375
+ const input = {
376
+ a: {
377
+ $ref: '#',
378
+ },
379
+ }
380
+
381
+ const proxied = createMagicProxy(input)
382
+
383
+ expect(() => {
384
+ proxied.a['$ref-value'] = 'new value'
385
+ }).toThrowError("'set' on proxy: trap returned falsish for property '$ref-value'")
386
+ })
387
+
388
+ // TODO: might change this behavior in the future
389
+ // so we allow setting the $ref-value for invalid refs by creating the path
390
+ it('throws when trying to update an invalid ref where the parent node does not exists', () => {
391
+ const input = {
392
+ a: {
393
+ $ref: '#/non-existent/some-path',
394
+ },
88
395
  b: {
89
396
  c: {
90
- d: {
91
- '$ref': '#/a/b/c/f',
397
+ hello: 'world',
398
+ },
399
+ },
400
+ }
401
+
402
+ const proxied = createMagicProxy(input)
403
+
404
+ expect(() => {
405
+ proxied.a['$ref-value'] = 'new value'
406
+ }).toThrowError("'set' on proxy: trap returned falsish for property '$ref-value'")
407
+ })
408
+ })
409
+
410
+ describe('has', () => {
411
+ it('returns true for $ref-value when $ref exists', () => {
412
+ const input = {
413
+ a: 'hello',
414
+ b: {
415
+ $ref: '#/a',
416
+ },
417
+ }
418
+
419
+ const result = createMagicProxy(input)
420
+ expect('$ref-value' in result.b).toBe(true)
421
+ })
422
+
423
+ it('returns false for $ref-value when $ref does not exist', () => {
424
+ const input = {
425
+ a: 'hello',
426
+ b: {
427
+ c: 'world',
428
+ },
429
+ }
430
+
431
+ const result = createMagicProxy(input)
432
+ expect('$ref-value' in result.b).toBe(false)
433
+ })
434
+
435
+ it('returns true for existing properties', () => {
436
+ const input = {
437
+ a: 'hello',
438
+ b: {
439
+ c: 'world',
440
+ },
441
+ }
442
+
443
+ const result = createMagicProxy(input)
444
+ expect('a' in result).toBe(true)
445
+ expect('b' in result).toBe(true)
446
+ expect('c' in result.b).toBe(true)
447
+ })
448
+
449
+ it('returns false for non-existent properties', () => {
450
+ const input = {
451
+ a: 'hello',
452
+ }
453
+
454
+ const result = createMagicProxy(input)
455
+ expect('nonExistent' in result).toBe(false)
456
+ expect('$ref-value' in result).toBe(false)
457
+ })
458
+
459
+ it('handles nested objects with refs', () => {
460
+ const input = {
461
+ a: {
462
+ b: {
463
+ c: 'hello',
464
+ },
465
+ },
466
+ d: {
467
+ $ref: '#/a/b',
468
+ },
469
+ }
470
+
471
+ const result = createMagicProxy(input)
472
+ expect('$ref-value' in result.d).toBe(true)
473
+ expect('c' in result.a.b).toBe(true)
474
+ })
475
+
476
+ it('handles arrays with refs', () => {
477
+ const input = {
478
+ items: [{ name: 'item1' }, { $ref: '#/items/0' }],
479
+ }
480
+
481
+ const result = createMagicProxy(input)
482
+ expect('$ref-value' in result.items[1]).toBe(true)
483
+ expect('name' in result.items[0]).toBe(true)
484
+ })
485
+
486
+ it('handles deeply nested refs', () => {
487
+ const input = {
488
+ a: {
489
+ b: {
490
+ c: {
491
+ d: {
492
+ e: 'hello',
493
+ },
92
494
  },
93
- e: {
94
- '$ref': '#/a/b/c/d',
495
+ },
496
+ },
497
+ f: {
498
+ $ref: '#/a/b/c/d',
499
+ },
500
+ }
501
+
502
+ const result = createMagicProxy(input)
503
+ expect('$ref-value' in result.f).toBe(true)
504
+ })
505
+
506
+ it('handles objects with both $ref and other properties', () => {
507
+ const input = {
508
+ a: {
509
+ b: {
510
+ c: 'hello',
511
+ },
512
+ },
513
+ d: {
514
+ $ref: '#/a/b',
515
+ extraProp: 'extra',
516
+ },
517
+ }
518
+
519
+ const result = createMagicProxy(input)
520
+ expect('$ref-value' in result.d).toBe(true)
521
+ expect('extraProp' in result.d).toBe(true)
522
+ expect('$ref' in result.d).toBe(true)
523
+ })
524
+
525
+ it('handles external refs', () => {
526
+ const input = {
527
+ a: 'hello',
528
+ b: {
529
+ $ref: 'https://example.com/document.json/#',
530
+ },
531
+ }
532
+
533
+ const result = createMagicProxy(input)
534
+ expect('$ref-value' in result.b).toBe(true)
535
+ })
536
+
537
+ it('handles empty objects', () => {
538
+ const input = {}
539
+
540
+ const result = createMagicProxy(input)
541
+ expect('$ref-value' in result).toBe(false)
542
+ expect('anyProperty' in result).toBe(false)
543
+ })
544
+
545
+ it('handles null and undefined values', () => {
546
+ const input = {
547
+ a: null,
548
+ b: undefined,
549
+ c: {
550
+ $ref: '#/a',
551
+ },
552
+ }
553
+
554
+ const result = createMagicProxy(input)
555
+ expect('$ref-value' in result.c).toBe(true)
556
+ expect('a' in result).toBe(true)
557
+ expect('b' in result).toBe(true)
558
+ })
559
+ })
560
+
561
+ describe('delete', () => {
562
+ it('deletes properties from the target object', () => {
563
+ const input = {
564
+ a: 'hello',
565
+ b: 'world',
566
+ c: 'test',
567
+ }
568
+
569
+ const result = createMagicProxy(input)
570
+ delete result.b
571
+
572
+ expect('b' in result).toBe(false)
573
+ expect(result.a).toBe('hello')
574
+ expect(result.c).toBe('test')
575
+ })
576
+
577
+ it('deletes properties from nested objects', () => {
578
+ const input = {
579
+ a: {
580
+ b: 'hello',
581
+ c: 'world',
582
+ d: 'test',
583
+ },
584
+ }
585
+
586
+ const result = createMagicProxy(input)
587
+ delete result.a.c
588
+
589
+ expect('c' in result.a).toBe(false)
590
+ expect(result.a.b).toBe('hello')
591
+ expect(result.a.d).toBe('test')
592
+ })
593
+
594
+ it('deletes properties from objects with refs', () => {
595
+ const input = {
596
+ a: {
597
+ b: 'hello',
598
+ c: 'world',
599
+ },
600
+ d: {
601
+ $ref: '#/a',
602
+ extraProp: 'extra',
603
+ },
604
+ }
605
+
606
+ const result = createMagicProxy(input)
607
+ delete result.d.extraProp
608
+
609
+ expect('extraProp' in result.d).toBe(false)
610
+ expect('$ref' in result.d).toBe(true)
611
+ expect('$ref-value' in result.d).toBe(true)
612
+ })
613
+
614
+ it('deletes properties from ref-value objects', () => {
615
+ const input = {
616
+ a: {
617
+ b: {
618
+ c: 'hello',
619
+ d: 'world',
620
+ },
621
+ },
622
+ e: {
623
+ $ref: '#/a/b',
624
+ },
625
+ }
626
+
627
+ const result = createMagicProxy(input)
628
+ delete result.e['$ref-value'].d
629
+
630
+ expect('d' in result.e['$ref-value']).toBe(false)
631
+ expect(result.e['$ref-value'].c).toBe('hello')
632
+ expect(input.a.b.d).toBeUndefined()
633
+ })
634
+
635
+ it('deletes array elements', () => {
636
+ const input = {
637
+ items: ['a', 'b', 'c', 'd'],
638
+ }
639
+
640
+ const result = createMagicProxy(input)
641
+ delete result.items[1]
642
+
643
+ expect(result.items[1]).toBeUndefined()
644
+ expect(result.items[0]).toBe('a')
645
+ expect(result.items[2]).toBe('c')
646
+ expect(result.items[3]).toBe('d')
647
+ })
648
+
649
+ it('deletes properties from array elements with refs', () => {
650
+ const input = {
651
+ $defs: {
652
+ item: { name: 'default', id: 123, type: 'test' },
653
+ },
654
+ items: [{ $ref: '#/$defs/item' }],
655
+ }
656
+
657
+ const result = createMagicProxy(input)
658
+ delete result.items[0]['$ref-value'].type
659
+
660
+ expect('type' in result.items[0]['$ref-value']).toBe(false)
661
+ expect(result.items[0]['$ref-value'].name).toBe('default')
662
+ expect(result.items[0]['$ref-value'].id).toBe(123)
663
+ })
664
+
665
+ it('deletes deeply nested properties', () => {
666
+ const input = {
667
+ a: {
668
+ b: {
669
+ c: {
670
+ d: {
671
+ e: 'hello',
672
+ f: 'world',
673
+ },
95
674
  },
96
- f: {
97
- someProp: 'test',
675
+ },
676
+ },
677
+ }
678
+
679
+ const result = createMagicProxy(input)
680
+ delete result.a.b.c.d.f
681
+
682
+ expect('f' in result.a.b.c.d).toBe(false)
683
+ expect(result.a.b.c.d.e).toBe('hello')
684
+ })
685
+
686
+ it('deletes properties from deeply nested refs', () => {
687
+ const input = {
688
+ a: {
689
+ b: {
690
+ c: {
691
+ d: {
692
+ e: 'hello',
693
+ f: 'world',
694
+ },
98
695
  },
99
696
  },
100
697
  },
101
- },
102
- }
698
+ g: {
699
+ $ref: '#/a/b/c/d',
700
+ },
701
+ }
702
+
703
+ const result = createMagicProxy(input)
704
+ delete result.g['$ref-value'].f
103
705
 
104
- const result = createMagicProxy(input)
105
- expect(result.a.b.c.e).toEqual({
106
- someProp: 'test',
107
- 'x-original-ref': '#/a/b/c/d',
706
+ expect('f' in result.g['$ref-value']).toBe(false)
707
+ expect(result.g['$ref-value'].e).toBe('hello')
708
+ expect(input.a.b.c.d.f).toBeUndefined()
709
+ })
710
+
711
+ it('deletes root level properties', () => {
712
+ const input: any = {
713
+ rootProp: 'root value',
714
+ nestedProp: 'nested value',
715
+ }
716
+
717
+ const result = createMagicProxy(input)
718
+ delete result.rootProp
719
+
720
+ expect('rootProp' in result).toBe(false)
721
+ expect(result.nestedProp).toBe('nested value')
722
+ })
723
+
724
+ it('deletes properties that do not exist', () => {
725
+ const input: any = {
726
+ a: 'hello',
727
+ }
728
+
729
+ const result = createMagicProxy(input)
730
+ const deleteResult = delete result.nonExistent
731
+
732
+ expect(deleteResult).toBe(true)
733
+ expect('nonExistent' in result).toBe(false)
734
+ })
735
+
736
+ it('deletes properties from objects with multiple refs', () => {
737
+ const input = {
738
+ a: {
739
+ b: {
740
+ c: 'hello',
741
+ d: 'world',
742
+ },
743
+ },
744
+ e: {
745
+ $ref: '#/a/b',
746
+ },
747
+ f: {
748
+ $ref: '#/e',
749
+ },
750
+ }
751
+
752
+ const result = createMagicProxy(input)
753
+ delete result.f['$ref-value']['$ref-value'].d
754
+
755
+ expect('d' in result.f['$ref-value']['$ref-value']).toBe(false)
756
+ expect(result.f['$ref-value']['$ref-value'].c).toBe('hello')
757
+ expect(input.a.b.d).toBeUndefined()
758
+ })
759
+
760
+ it('deletes properties from external refs', () => {
761
+ const input = {
762
+ a: 'hello',
763
+ b: {
764
+ $ref: 'https://example.com/document.json/#',
765
+ localProp: 'local',
766
+ },
767
+ }
768
+
769
+ const result = createMagicProxy(input)
770
+ delete result.b.localProp
771
+
772
+ expect('localProp' in result.b).toBe(false)
773
+ expect('$ref' in result.b).toBe(true)
774
+ expect('$ref-value' in result.b).toBe(true)
775
+ })
776
+
777
+ it('deletes properties from empty objects', () => {
778
+ const input: any = {}
779
+
780
+ const result = createMagicProxy(input)
781
+ const deleteResult = delete result.anyProperty
782
+
783
+ expect(deleteResult).toBe(true)
784
+ expect('anyProperty' in result).toBe(false)
785
+ })
786
+
787
+ it('deletes properties from objects with null and undefined values', () => {
788
+ const input = {
789
+ a: null,
790
+ b: undefined,
791
+ c: {
792
+ d: null,
793
+ e: undefined,
794
+ },
795
+ }
796
+
797
+ const result = createMagicProxy(input)
798
+ delete result.a
799
+ delete result.c.d
800
+
801
+ expect('a' in result).toBe(false)
802
+ expect('d' in result.c).toBe(false)
803
+ expect('b' in result).toBe(true)
804
+ expect('e' in result.c).toBe(true)
805
+ })
806
+
807
+ it('deletes properties from arrays with mixed content', () => {
808
+ const input: any = {
809
+ items: [{ name: 'item1', id: 1 }, { $ref: '#/items/0' }, 'string item', { name: 'item2', id: 2 }],
810
+ }
811
+
812
+ const result = createMagicProxy(input)
813
+ delete result.items[0].id
814
+ delete result.items[1]['$ref-value'].name
815
+
816
+ expect('id' in result.items[0]).toBe(false)
817
+ expect('name' in result.items[1]['$ref-value']).toBe(false)
818
+ expect(result.items[0].name).toBe(undefined)
819
+ expect(result.items[2]).toBe('string item')
820
+ })
821
+
822
+ it('deletes properties from objects with both direct and ref properties', () => {
823
+ const input = {
824
+ a: {
825
+ b: {
826
+ c: 'hello',
827
+ d: 'world',
828
+ },
829
+ },
830
+ e: {
831
+ $ref: '#/a/b',
832
+ directProp: 'direct',
833
+ },
834
+ }
835
+
836
+ const result = createMagicProxy(input)
837
+ delete result.e.directProp
838
+ delete result.e['$ref-value'].d
839
+
840
+ expect('directProp' in result.e).toBe(false)
841
+ expect('d' in result.e['$ref-value']).toBe(false)
842
+ expect('$ref' in result.e).toBe(true)
843
+ expect(result.e['$ref-value'].c).toBe('hello')
108
844
  })
109
845
  })
110
846
 
111
- it('should resolve the references inside an array', () => {
112
- const input = {
113
- $defs: {
847
+ describe('ownKeys', () => {
848
+ it('returns original keys when no $ref exists', () => {
849
+ const input = {
850
+ a: 'hello',
851
+ b: 'world',
852
+ c: 'test',
853
+ }
854
+
855
+ const result = createMagicProxy(input)
856
+ const keys = Object.keys(result)
857
+
858
+ expect(keys).toEqual(['a', 'b', 'c'])
859
+ })
860
+
861
+ it('includes $ref-value when $ref exists', () => {
862
+ const input = {
863
+ a: 'hello',
864
+ b: {
865
+ $ref: '#/a',
866
+ },
867
+ }
868
+
869
+ const result = createMagicProxy(input)
870
+ const keys = Object.keys(result.b)
871
+
872
+ expect(keys).toEqual(['$ref', '$ref-value'])
873
+ })
874
+
875
+ it('does not duplicate $ref-value if it already exists', () => {
876
+ const input = {
877
+ a: 'hello',
878
+ b: {
879
+ $ref: '#/a',
880
+ '$ref-value': 'existing',
881
+ },
882
+ }
883
+
884
+ const result = createMagicProxy(input)
885
+ const keys = Object.keys(result.b)
886
+
887
+ expect(keys).toEqual(['$ref', '$ref-value'])
888
+ })
889
+
890
+ it('handles nested objects with refs', () => {
891
+ const input = {
114
892
  a: {
115
893
  b: {
116
- prop: 'hello',
894
+ c: 'hello',
117
895
  },
118
- c: {
119
- someOtherProp: 'world',
120
- },
121
- },
122
- },
123
- a: {
124
- b: [
125
- {
126
- $ref: '#/$defs/a/b',
127
- },
128
- {
129
- $ref: '#/$defs/a/c',
130
- },
131
- ],
132
- },
133
- }
134
-
135
- const result = createMagicProxy(input)
136
- expect(JSON.stringify(result.a)).toEqual(
137
- JSON.stringify({
138
- 'b': [
139
- { 'prop': 'hello', 'x-original-ref': '#/$defs/a/b' },
140
- { 'someOtherProp': 'world', 'x-original-ref': '#/$defs/a/c' },
141
- ],
142
- }),
143
- )
896
+ },
897
+ d: {
898
+ $ref: '#/a/b',
899
+ extraProp: 'extra',
900
+ },
901
+ }
902
+
903
+ const result = createMagicProxy(input)
904
+ const keys = Object.keys(result.d)
905
+
906
+ expect(keys).toEqual(['$ref', 'extraProp', '$ref-value'])
907
+ })
908
+
909
+ it('handles arrays with refs', () => {
910
+ const input = {
911
+ items: [{ name: 'item1' }, { $ref: '#/items/0' }, { name: 'item2', id: 2 }],
912
+ }
913
+
914
+ const result = createMagicProxy(input)
915
+ const keys = Object.keys(result.items[1])
916
+
917
+ expect(keys).toEqual(['$ref', '$ref-value'])
918
+ })
919
+
920
+ it('handles deeply nested refs', () => {
921
+ const input = {
922
+ a: {
923
+ b: {
924
+ c: {
925
+ d: {
926
+ e: 'hello',
927
+ },
928
+ },
929
+ },
930
+ },
931
+ f: {
932
+ $ref: '#/a/b/c/d',
933
+ },
934
+ }
935
+
936
+ const result = createMagicProxy(input)
937
+ const keys = Object.keys(result.f)
938
+
939
+ expect(keys).toEqual(['$ref', '$ref-value'])
940
+ })
941
+
942
+ it('handles objects with multiple refs', () => {
943
+ const input = {
944
+ a: {
945
+ b: {
946
+ c: 'hello',
947
+ },
948
+ },
949
+ d: {
950
+ $ref: '#/a/b',
951
+ },
952
+ e: {
953
+ $ref: '#/d',
954
+ },
955
+ }
956
+
957
+ const result = createMagicProxy(input)
958
+ const dKeys = Object.keys(result.d)
959
+ const eKeys = Object.keys(result.e)
960
+
961
+ expect(dKeys).toEqual(['$ref', '$ref-value'])
962
+ expect(eKeys).toEqual(['$ref', '$ref-value'])
963
+ })
964
+
965
+ it('handles external refs', () => {
966
+ const input = {
967
+ a: 'hello',
968
+ b: {
969
+ $ref: 'https://example.com/document.json/#',
970
+ },
971
+ }
972
+
973
+ const result = createMagicProxy(input)
974
+ const keys = Object.keys(result.b)
975
+
976
+ expect(keys).toEqual(['$ref', '$ref-value'])
977
+ })
978
+
979
+ it('handles empty objects', () => {
980
+ const input = {}
981
+
982
+ const result = createMagicProxy(input)
983
+ const keys = Object.keys(result)
984
+
985
+ expect(keys).toEqual([])
986
+ })
987
+
988
+ it('handles objects with only $ref', () => {
989
+ const input = {
990
+ a: {
991
+ $ref: '#/b',
992
+ },
993
+ b: 'hello',
994
+ }
995
+
996
+ const result = createMagicProxy(input)
997
+ const keys = Object.keys(result.a)
998
+
999
+ expect(keys).toEqual(['$ref', '$ref-value'])
1000
+ })
1001
+
1002
+ it('handles objects with null and undefined values', () => {
1003
+ const input = {
1004
+ a: null,
1005
+ b: undefined,
1006
+ c: {
1007
+ $ref: '#/a',
1008
+ },
1009
+ d: {
1010
+ $ref: '#/b',
1011
+ },
1012
+ }
1013
+
1014
+ const result = createMagicProxy(input)
1015
+ const cKeys = Object.keys(result.c)
1016
+ const dKeys = Object.keys(result.d)
1017
+
1018
+ expect(cKeys).toEqual(['$ref', '$ref-value'])
1019
+ expect(dKeys).toEqual(['$ref', '$ref-value'])
1020
+ })
1021
+
1022
+ it('handles arrays with mixed content', () => {
1023
+ const input = {
1024
+ items: [{ name: 'item1' }, { $ref: '#/items/0' }, 'string item', { name: 'item2', id: 2 }],
1025
+ }
1026
+
1027
+ const result = createMagicProxy(input)
1028
+ const item0Keys = Object.keys(result.items[0])
1029
+ const item1Keys = Object.keys(result.items[1])
1030
+
1031
+ expect(item0Keys).toEqual(['name'])
1032
+ expect(item1Keys).toEqual(['$ref', '$ref-value'])
1033
+ })
1034
+
1035
+ it('handles objects with both direct and ref properties', () => {
1036
+ const input = {
1037
+ a: {
1038
+ b: {
1039
+ c: 'hello',
1040
+ },
1041
+ },
1042
+ d: {
1043
+ $ref: '#/a/b',
1044
+ directProp: 'direct',
1045
+ anotherProp: 'another',
1046
+ },
1047
+ }
1048
+
1049
+ const result = createMagicProxy(input)
1050
+ const keys = Object.keys(result.d)
1051
+
1052
+ expect(keys).toEqual(['$ref', 'directProp', 'anotherProp', '$ref-value'])
1053
+ })
1054
+
1055
+ it('handles objects with symbol keys', () => {
1056
+ const symbolKey = Symbol('test')
1057
+ const input = {
1058
+ [symbolKey]: 'symbol value',
1059
+ a: 'hello',
1060
+ b: {
1061
+ $ref: '#/a',
1062
+ },
1063
+ }
1064
+
1065
+ const result = createMagicProxy(input)
1066
+ const keys = Object.keys(result)
1067
+ const ownKeys = Object.getOwnPropertySymbols(result)
1068
+
1069
+ expect(keys).toEqual(['a', 'b'])
1070
+ expect(ownKeys).toEqual([symbolKey])
1071
+ })
1072
+
1073
+ it('handles objects with non-enumerable properties', () => {
1074
+ const input = {
1075
+ a: 'hello',
1076
+ b: {
1077
+ $ref: '#/a',
1078
+ },
1079
+ }
1080
+
1081
+ Object.defineProperty(input.b, 'nonEnumerable', {
1082
+ value: 'test',
1083
+ enumerable: false,
1084
+ })
1085
+
1086
+ const result = createMagicProxy(input)
1087
+ const keys = Object.keys(result.b)
1088
+ const ownKeys = Reflect.ownKeys(result.b)
1089
+
1090
+ expect(keys).toEqual(['$ref', '$ref-value'])
1091
+ expect(ownKeys).toContain('nonEnumerable')
1092
+ expect(ownKeys).toContain('$ref')
1093
+ expect(ownKeys).toContain('$ref-value')
1094
+ })
1095
+
1096
+ it('handles objects with getter properties', () => {
1097
+ const input = {
1098
+ a: 'hello',
1099
+ b: {
1100
+ $ref: '#/a',
1101
+ },
1102
+ }
1103
+
1104
+ Object.defineProperty(input.b, 'getterProp', {
1105
+ get() {
1106
+ return 'getter value'
1107
+ },
1108
+ enumerable: true,
1109
+ })
1110
+
1111
+ const result = createMagicProxy(input)
1112
+ const keys = Object.keys(result.b)
1113
+
1114
+ expect(keys).toEqual(['$ref', 'getterProp', '$ref-value'])
1115
+ })
1116
+ })
1117
+
1118
+ describe('getRaw', () => {
1119
+ it('should get the raw version of the document', () => {
1120
+ const input = {
1121
+ a: 'hello',
1122
+ b: {
1123
+ $ref: '#/a',
1124
+ },
1125
+ }
1126
+
1127
+ const proxied = createMagicProxy(input)
1128
+
1129
+ expect(proxied).toEqual({
1130
+ a: 'hello',
1131
+ b: {
1132
+ $ref: '#/a',
1133
+ '$ref-value': 'hello',
1134
+ },
1135
+ })
1136
+
1137
+ expect(getRaw(proxied)).toEqual(input)
1138
+ })
144
1139
  })
145
1140
  })