@scalar/json-magic 0.1.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 (185) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +356 -0
  5. package/dist/bundle/bundle.d.ts +292 -0
  6. package/dist/bundle/bundle.d.ts.map +1 -0
  7. package/dist/bundle/bundle.js +259 -0
  8. package/dist/bundle/bundle.js.map +7 -0
  9. package/dist/bundle/create-limiter.d.ts +21 -0
  10. package/dist/bundle/create-limiter.d.ts.map +1 -0
  11. package/dist/bundle/create-limiter.js +31 -0
  12. package/dist/bundle/create-limiter.js.map +7 -0
  13. package/dist/bundle/index.d.ts +2 -0
  14. package/dist/bundle/index.d.ts.map +1 -0
  15. package/dist/bundle/index.js +5 -0
  16. package/dist/bundle/index.js.map +7 -0
  17. package/dist/bundle/plugins/browser.d.ts +4 -0
  18. package/dist/bundle/plugins/browser.d.ts.map +1 -0
  19. package/dist/bundle/plugins/browser.js +9 -0
  20. package/dist/bundle/plugins/browser.js.map +7 -0
  21. package/dist/bundle/plugins/fetch-urls/index.d.ts +39 -0
  22. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -0
  23. package/dist/bundle/plugins/fetch-urls/index.js +48 -0
  24. package/dist/bundle/plugins/fetch-urls/index.js.map +7 -0
  25. package/dist/bundle/plugins/node.d.ts +5 -0
  26. package/dist/bundle/plugins/node.d.ts.map +1 -0
  27. package/dist/bundle/plugins/node.js +11 -0
  28. package/dist/bundle/plugins/node.js.map +7 -0
  29. package/dist/bundle/plugins/parse-json/index.d.ts +13 -0
  30. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -0
  31. package/dist/bundle/plugins/parse-json/index.js +22 -0
  32. package/dist/bundle/plugins/parse-json/index.js.map +7 -0
  33. package/dist/bundle/plugins/parse-yaml/index.d.ts +13 -0
  34. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -0
  35. package/dist/bundle/plugins/parse-yaml/index.js +23 -0
  36. package/dist/bundle/plugins/parse-yaml/index.js.map +7 -0
  37. package/dist/bundle/plugins/read-files/index.d.ts +29 -0
  38. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -0
  39. package/dist/bundle/plugins/read-files/index.js +30 -0
  40. package/dist/bundle/plugins/read-files/index.js.map +7 -0
  41. package/dist/bundle/value-generator.d.ts +79 -0
  42. package/dist/bundle/value-generator.d.ts.map +1 -0
  43. package/dist/bundle/value-generator.js +55 -0
  44. package/dist/bundle/value-generator.js.map +7 -0
  45. package/dist/dereference/dereference.d.ts +45 -0
  46. package/dist/dereference/dereference.d.ts.map +1 -0
  47. package/dist/dereference/dereference.js +37 -0
  48. package/dist/dereference/dereference.js.map +7 -0
  49. package/dist/dereference/index.d.ts +2 -0
  50. package/dist/dereference/index.d.ts.map +1 -0
  51. package/dist/dereference/index.js +5 -0
  52. package/dist/dereference/index.js.map +7 -0
  53. package/dist/diff/apply.d.ts +35 -0
  54. package/dist/diff/apply.d.ts.map +1 -0
  55. package/dist/diff/apply.js +40 -0
  56. package/dist/diff/apply.js.map +7 -0
  57. package/dist/diff/diff.d.ts +56 -0
  58. package/dist/diff/diff.d.ts.map +1 -0
  59. package/dist/diff/diff.js +33 -0
  60. package/dist/diff/diff.js.map +7 -0
  61. package/dist/diff/index.d.ts +5 -0
  62. package/dist/diff/index.d.ts.map +1 -0
  63. package/dist/diff/index.js +9 -0
  64. package/dist/diff/index.js.map +7 -0
  65. package/dist/diff/merge.d.ts +43 -0
  66. package/dist/diff/merge.d.ts.map +1 -0
  67. package/dist/diff/merge.js +61 -0
  68. package/dist/diff/merge.js.map +7 -0
  69. package/dist/diff/trie.d.ts +64 -0
  70. package/dist/diff/trie.d.ts.map +1 -0
  71. package/dist/diff/trie.js +82 -0
  72. package/dist/diff/trie.js.map +7 -0
  73. package/dist/diff/utils.d.ts +63 -0
  74. package/dist/diff/utils.d.ts.map +1 -0
  75. package/dist/diff/utils.js +48 -0
  76. package/dist/diff/utils.js.map +7 -0
  77. package/dist/magic-proxy/index.d.ts +2 -0
  78. package/dist/magic-proxy/index.d.ts.map +1 -0
  79. package/dist/magic-proxy/index.js +6 -0
  80. package/dist/magic-proxy/index.js.map +7 -0
  81. package/dist/magic-proxy/proxy.d.ts +63 -0
  82. package/dist/magic-proxy/proxy.d.ts.map +1 -0
  83. package/dist/magic-proxy/proxy.js +108 -0
  84. package/dist/magic-proxy/proxy.js.map +7 -0
  85. package/dist/polyfills/index.d.ts +2 -0
  86. package/dist/polyfills/index.d.ts.map +1 -0
  87. package/dist/polyfills/index.js +25 -0
  88. package/dist/polyfills/index.js.map +7 -0
  89. package/dist/polyfills/path.d.ts +24 -0
  90. package/dist/polyfills/path.d.ts.map +1 -0
  91. package/dist/polyfills/path.js +174 -0
  92. package/dist/polyfills/path.js.map +7 -0
  93. package/dist/types.d.ts +2 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +1 -0
  96. package/dist/types.js.map +7 -0
  97. package/dist/utils/escape-json-pointer.d.ts +7 -0
  98. package/dist/utils/escape-json-pointer.d.ts.map +1 -0
  99. package/dist/utils/escape-json-pointer.js +7 -0
  100. package/dist/utils/escape-json-pointer.js.map +7 -0
  101. package/dist/utils/get-segments-from-path.d.ts +5 -0
  102. package/dist/utils/get-segments-from-path.d.ts.map +1 -0
  103. package/dist/utils/get-segments-from-path.js +11 -0
  104. package/dist/utils/get-segments-from-path.js.map +7 -0
  105. package/dist/utils/is-json-object.d.ts +18 -0
  106. package/dist/utils/is-json-object.d.ts.map +1 -0
  107. package/dist/utils/is-json-object.js +16 -0
  108. package/dist/utils/is-json-object.js.map +7 -0
  109. package/dist/utils/is-object.d.ts +5 -0
  110. package/dist/utils/is-object.d.ts.map +1 -0
  111. package/dist/utils/is-object.js +5 -0
  112. package/dist/utils/is-object.js.map +7 -0
  113. package/dist/utils/is-yaml.d.ts +17 -0
  114. package/dist/utils/is-yaml.d.ts.map +1 -0
  115. package/dist/utils/is-yaml.js +7 -0
  116. package/dist/utils/is-yaml.js.map +7 -0
  117. package/dist/utils/json-path-utils.d.ts +23 -0
  118. package/dist/utils/json-path-utils.d.ts.map +1 -0
  119. package/dist/utils/json-path-utils.js +16 -0
  120. package/dist/utils/json-path-utils.js.map +7 -0
  121. package/dist/utils/normalize.d.ts +5 -0
  122. package/dist/utils/normalize.d.ts.map +1 -0
  123. package/dist/utils/normalize.js +28 -0
  124. package/dist/utils/normalize.js.map +7 -0
  125. package/dist/utils/unescape-json-pointer.d.ts +8 -0
  126. package/dist/utils/unescape-json-pointer.d.ts.map +1 -0
  127. package/dist/utils/unescape-json-pointer.js +7 -0
  128. package/dist/utils/unescape-json-pointer.js.map +7 -0
  129. package/esbuild.ts +13 -0
  130. package/package.json +65 -0
  131. package/src/bundle/bundle.test.ts +1843 -0
  132. package/src/bundle/bundle.ts +758 -0
  133. package/src/bundle/create-limiter.test.ts +28 -0
  134. package/src/bundle/create-limiter.ts +52 -0
  135. package/src/bundle/index.ts +2 -0
  136. package/src/bundle/plugins/browser.ts +4 -0
  137. package/src/bundle/plugins/fetch-urls/index.test.ts +147 -0
  138. package/src/bundle/plugins/fetch-urls/index.ts +94 -0
  139. package/src/bundle/plugins/node.ts +5 -0
  140. package/src/bundle/plugins/parse-json/index.test.ts +22 -0
  141. package/src/bundle/plugins/parse-json/index.ts +30 -0
  142. package/src/bundle/plugins/parse-yaml/index.test.ts +24 -0
  143. package/src/bundle/plugins/parse-yaml/index.ts +31 -0
  144. package/src/bundle/plugins/read-files/index.test.ts +35 -0
  145. package/src/bundle/plugins/read-files/index.ts +55 -0
  146. package/src/bundle/value-generator.test.ts +166 -0
  147. package/src/bundle/value-generator.ts +147 -0
  148. package/src/dereference/dereference.test.ts +137 -0
  149. package/src/dereference/dereference.ts +84 -0
  150. package/src/dereference/index.ts +2 -0
  151. package/src/diff/apply.test.ts +262 -0
  152. package/src/diff/apply.ts +78 -0
  153. package/src/diff/diff.test.ts +328 -0
  154. package/src/diff/diff.ts +94 -0
  155. package/src/diff/index.test.ts +150 -0
  156. package/src/diff/index.ts +5 -0
  157. package/src/diff/merge.test.ts +1109 -0
  158. package/src/diff/merge.ts +136 -0
  159. package/src/diff/trie.test.ts +30 -0
  160. package/src/diff/trie.ts +113 -0
  161. package/src/diff/utils.test.ts +169 -0
  162. package/src/diff/utils.ts +113 -0
  163. package/src/magic-proxy/index.ts +2 -0
  164. package/src/magic-proxy/proxy.test.ts +145 -0
  165. package/src/magic-proxy/proxy.ts +225 -0
  166. package/src/polyfills/index.ts +12 -0
  167. package/src/polyfills/path.ts +248 -0
  168. package/src/types.ts +1 -0
  169. package/src/utils/escape-json-pointer.test.ts +13 -0
  170. package/src/utils/escape-json-pointer.ts +8 -0
  171. package/src/utils/get-segments-from-path.test.ts +17 -0
  172. package/src/utils/get-segments-from-path.ts +17 -0
  173. package/src/utils/is-json-object.ts +31 -0
  174. package/src/utils/is-object.test.ts +27 -0
  175. package/src/utils/is-object.ts +4 -0
  176. package/src/utils/is-yaml.ts +18 -0
  177. package/src/utils/json-path-utils.test.ts +13 -0
  178. package/src/utils/json-path-utils.ts +38 -0
  179. package/src/utils/normalize.test.ts +91 -0
  180. package/src/utils/normalize.ts +34 -0
  181. package/src/utils/unescape-json-pointer.test.ts +23 -0
  182. package/src/utils/unescape-json-pointer.ts +9 -0
  183. package/tsconfig.build.json +12 -0
  184. package/tsconfig.json +16 -0
  185. package/vite.config.ts +8 -0
@@ -0,0 +1,262 @@
1
+ import { apply, InvalidChangesDetectedError } from '@/diff/apply'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ const deepClone = <T extends object>(obj: T) => JSON.parse(JSON.stringify(obj)) as T
5
+
6
+ describe('apply', () => {
7
+ describe('should apply `add` operations', () => {
8
+ test('should apply `add` operation correctly', () => {
9
+ const doc = {
10
+ name: 'John',
11
+ age: 25,
12
+ }
13
+
14
+ const docCopy = deepClone(doc)
15
+ const location = { city: 'New York', street: '5th Avenue' }
16
+
17
+ expect(apply(doc, [{ path: ['location'], changes: location, type: 'add' }])).toEqual({
18
+ ...docCopy,
19
+ location,
20
+ })
21
+ })
22
+
23
+ test('should apply `add` operation on deeply nested objects correctly', () => {
24
+ const doc = {
25
+ name: 'John',
26
+ age: 25,
27
+ location: {
28
+ city: 'New York',
29
+ street: '5th Avenue',
30
+ },
31
+ }
32
+
33
+ const docCopy = deepClone(doc)
34
+ const coordinates = { lat: 40.7128, long: 74.006 }
35
+
36
+ expect(
37
+ apply(doc, [
38
+ {
39
+ path: ['location', 'coordinates'],
40
+ changes: coordinates,
41
+ type: 'add',
42
+ },
43
+ ]),
44
+ ).toEqual({
45
+ ...docCopy,
46
+ location: {
47
+ ...docCopy.location,
48
+ coordinates,
49
+ },
50
+ })
51
+ })
52
+ })
53
+
54
+ describe('should apply `update` operation', () => {
55
+ test('should apply `update` operation correctly', () => {
56
+ const doc = {
57
+ name: 'John',
58
+ age: 25,
59
+ location: {
60
+ city: 'New York',
61
+ street: '5th Avenue',
62
+ },
63
+ }
64
+ const docCopy = deepClone(doc)
65
+ const updatedAge = 26
66
+
67
+ expect(apply(doc, [{ path: ['age'], changes: updatedAge, type: 'update' }])).toEqual({
68
+ ...docCopy,
69
+ age: updatedAge,
70
+ })
71
+ })
72
+
73
+ test('should apply `update` operation correctly on nested objects', () => {
74
+ const doc = {
75
+ name: 'John',
76
+ age: 25,
77
+ location: {
78
+ city: 'New York',
79
+ street: '5th Avenue',
80
+ },
81
+ }
82
+
83
+ const docCopy = deepClone(doc)
84
+ const updatedCity = 'Boston'
85
+
86
+ expect(apply(doc, [{ path: ['location', 'city'], changes: updatedCity, type: 'update' }])).toEqual({
87
+ ...docCopy,
88
+ location: { ...docCopy.location, city: updatedCity },
89
+ })
90
+ })
91
+ })
92
+
93
+ describe('should apply `delete` operation', () => {
94
+ test('should apply `delete` operation correctly', () => {
95
+ const doc2 = {
96
+ name: 'John',
97
+ age: 25,
98
+ }
99
+
100
+ const doc1 = {
101
+ ...doc2,
102
+ location: {
103
+ city: 'New York',
104
+ street: '5th Avenue',
105
+ },
106
+ }
107
+
108
+ expect(apply(doc1, [{ path: ['location'], changes: doc1.location, type: 'delete' }])).toEqual(doc2)
109
+ })
110
+
111
+ test('should apply `delete` operation correctly on nested objects', () => {
112
+ const doc2 = {
113
+ name: 'John',
114
+ age: 25,
115
+ location: {
116
+ city: 'New York',
117
+ },
118
+ }
119
+
120
+ const doc1 = {
121
+ ...doc2,
122
+ location: {
123
+ ...doc2.location,
124
+ street: '5th Avenue',
125
+ },
126
+ }
127
+
128
+ expect(
129
+ apply(doc1, [
130
+ {
131
+ path: ['location', 'street'],
132
+ changes: doc1.location.street,
133
+ type: 'delete',
134
+ },
135
+ ]),
136
+ ).toEqual(doc2)
137
+ })
138
+ })
139
+
140
+ describe('should throw on incorrect diff', () => {
141
+ test('wrong nested key', () => {
142
+ const doc = {
143
+ name: 'John',
144
+ age: 25,
145
+ location: {
146
+ city: 'New York',
147
+ street: '5th Avenue',
148
+ },
149
+ }
150
+
151
+ expect(() =>
152
+ apply(doc, [
153
+ {
154
+ path: ['location', 'city', 'something'],
155
+ changes: { test: 1 },
156
+ type: 'add',
157
+ },
158
+ ]),
159
+ ).toThrow(InvalidChangesDetectedError)
160
+ })
161
+
162
+ test('wrong non existing path', () => {
163
+ const doc = {
164
+ name: 'John',
165
+ age: 25,
166
+ location: {
167
+ city: 'New York',
168
+ street: '5th Avenue',
169
+ },
170
+ }
171
+
172
+ expect(() =>
173
+ apply(doc, [
174
+ {
175
+ path: ['location', 'coordinates', 'lang'],
176
+ changes: 41.25,
177
+ type: 'add',
178
+ },
179
+ ]),
180
+ ).toThrow(InvalidChangesDetectedError)
181
+ })
182
+ })
183
+
184
+ describe('should correctly handle arrays', () => {
185
+ test('should correctly apply `add` changes on an array', () => {
186
+ const doc = {
187
+ name: 'John',
188
+ age: 25,
189
+ location: {
190
+ city: 'New York',
191
+ street: '5th Avenue',
192
+ },
193
+ hobbies: ['swimming'],
194
+ }
195
+
196
+ const docCopy = deepClone(doc)
197
+ const newHobby = 'coding'
198
+
199
+ expect(
200
+ apply(doc, [
201
+ {
202
+ path: ['hobbies', '1'],
203
+ changes: newHobby,
204
+ type: 'add',
205
+ },
206
+ ]),
207
+ ).toEqual({ ...docCopy, hobbies: [...docCopy.hobbies, newHobby] })
208
+ })
209
+ })
210
+
211
+ test('should correctly apply `update` changes on an array', () => {
212
+ const doc = {
213
+ name: 'John',
214
+ age: 25,
215
+ location: {
216
+ city: 'New York',
217
+ street: '5th Avenue',
218
+ },
219
+ hobbies: ['swimming', 'fish', 'coding'],
220
+ }
221
+
222
+ const docCopy = deepClone(doc)
223
+ const updatedHobby = 'running'
224
+ docCopy.hobbies[1] = updatedHobby
225
+
226
+ expect(
227
+ apply(doc, [
228
+ {
229
+ path: ['hobbies', '1'],
230
+ changes: updatedHobby,
231
+ type: 'update',
232
+ },
233
+ ]),
234
+ ).toEqual(docCopy)
235
+ })
236
+
237
+ test('should correctly apply `delete` changes on an array', () => {
238
+ const doc = {
239
+ name: 'John',
240
+ age: 25,
241
+ location: {
242
+ city: 'New York',
243
+ street: '5th Avenue',
244
+ },
245
+ hobbies: ['swimming', 'fish', 'coding'],
246
+ }
247
+
248
+ const docCopy = deepClone(doc)
249
+ // Perform the delete operation
250
+ docCopy.hobbies.splice(1, 1)
251
+
252
+ expect(
253
+ apply(doc, [
254
+ {
255
+ path: ['hobbies', '1'],
256
+ changes: doc.hobbies[1],
257
+ type: 'delete',
258
+ },
259
+ ]),
260
+ ).toEqual(docCopy)
261
+ })
262
+ })
@@ -0,0 +1,78 @@
1
+ import type { Difference } from '@/diff/diff'
2
+
3
+ export class InvalidChangesDetectedError extends Error {
4
+ constructor(message: string) {
5
+ super(message)
6
+ this.name = 'InvalidChangesDetectedError'
7
+ }
8
+ }
9
+
10
+ /**
11
+ * Applies a set of differences to a document object.
12
+ * The function traverses the document structure following the paths specified in the differences
13
+ * and applies the corresponding changes (add, update, or delete) at each location.
14
+ *
15
+ * @param document - The original document to apply changes to
16
+ * @param diff - Array of differences to apply, each containing a path and change type
17
+ * @returns The modified document with all changes applied
18
+ *
19
+ * @example
20
+ * const original = {
21
+ * paths: {
22
+ * '/users': {
23
+ * get: { responses: { '200': { description: 'OK' } } }
24
+ * }
25
+ * }
26
+ * }
27
+ *
28
+ * const changes = [
29
+ * {
30
+ * path: ['paths', '/users', 'get', 'responses', '200', 'content'],
31
+ * type: 'add',
32
+ * changes: { 'application/json': { schema: { type: 'object' } } }
33
+ * }
34
+ * ]
35
+ *
36
+ * const updated = apply(original, changes)
37
+ * // Result: original document with content added to the 200 response
38
+ */
39
+ export const apply = (document: Record<string, unknown>, diff: Difference[]): Record<string, unknown> => {
40
+ // Traverse the object and apply the change
41
+ const applyChange = (current: any, path: string[], d: Difference, depth = 0) => {
42
+ if (path[depth] === undefined) {
43
+ throw new InvalidChangesDetectedError(
44
+ `Process aborted. Path ${path.join('.')} at depth ${depth} is undefined, check diff object`,
45
+ )
46
+ }
47
+
48
+ // We reach where we want to be, now we can apply changes
49
+ if (depth >= path.length - 1) {
50
+ if (d.type === 'add' || d.type === 'update') {
51
+ current[path[depth]] = d.changes
52
+ } else {
53
+ // For arrays we don't use delete operator since it will leave blank spots and not actually remove the element
54
+ if (Array.isArray(current)) {
55
+ current.splice(Number.parseInt(path[depth]), 1)
56
+ } else {
57
+ delete current[path[depth]]
58
+ }
59
+ }
60
+ return
61
+ }
62
+
63
+ // Throw an error
64
+ // This scenario should not happen
65
+ // 1- if we are adding a new entry, the diff should only give us the higher level diff
66
+ // 2- if we are updating/deleting an entry, the path to that entry should exists
67
+ if (current[path[depth]] === undefined || typeof current[path[depth]] !== 'object') {
68
+ throw new InvalidChangesDetectedError('Process aborted, check diff object')
69
+ }
70
+ applyChange(current[path[depth]], path, d, depth + 1)
71
+ }
72
+
73
+ for (const d of diff) {
74
+ applyChange(document, d.path, d)
75
+ }
76
+
77
+ return document
78
+ }
@@ -0,0 +1,328 @@
1
+ import { diff } from '@/diff'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ describe('diff', () => {
5
+ describe('Should correctly detect `add` type diff', () => {
6
+ test('should correctly get added properties between two json objects', () => {
7
+ const doc1 = {
8
+ name: 'John',
9
+ age: 25,
10
+ }
11
+
12
+ const doc2 = {
13
+ ...doc1,
14
+ address: {
15
+ city: 'New York',
16
+ street: '5th Avenue',
17
+ },
18
+ }
19
+
20
+ expect(diff(doc1, doc2)).toEqual([{ path: ['address'], changes: doc2.address, type: 'add' }])
21
+ })
22
+
23
+ test('should correctly get added properties in nested objects between two json objects', () => {
24
+ const doc1 = {
25
+ name: 'John',
26
+ age: 25,
27
+ address: {
28
+ city: 'New York',
29
+ street: '5th Avenue',
30
+ },
31
+ }
32
+
33
+ const doc2 = {
34
+ ...doc1,
35
+ address: {
36
+ ...doc1.address,
37
+ coordinates: {
38
+ lat: 40.7128,
39
+ long: 74.006,
40
+ },
41
+ },
42
+ }
43
+
44
+ expect(diff(doc1, doc2)).toEqual([
45
+ {
46
+ path: ['address', 'coordinates'],
47
+ changes: doc2.address.coordinates,
48
+ type: 'add',
49
+ },
50
+ ])
51
+ })
52
+
53
+ test('should correctly get added properties in deeply nested objects between two json objects', () => {
54
+ const doc1 = {
55
+ name: 'John',
56
+ age: 25,
57
+ address: {
58
+ city: 'New York',
59
+ street: '5th Avenue',
60
+ coordinates: {
61
+ lat: 40.7128,
62
+ },
63
+ },
64
+ }
65
+
66
+ const doc2 = {
67
+ ...doc1,
68
+ address: {
69
+ ...doc1.address,
70
+ coordinates: {
71
+ ...doc1.address.coordinates,
72
+ long: 74.006,
73
+ },
74
+ },
75
+ }
76
+
77
+ expect(diff(doc1, doc2)).toEqual([
78
+ {
79
+ path: ['address', 'coordinates', 'long'],
80
+ changes: doc2.address.coordinates.long,
81
+ type: 'add',
82
+ },
83
+ ])
84
+ })
85
+ })
86
+
87
+ describe('Should correctly detect `update` type diff', () => {
88
+ test('should correctly get updates on primitives between two objects', () => {
89
+ const doc1 = {
90
+ name: 'John',
91
+ age: 25,
92
+ address: {
93
+ city: 'New York',
94
+ street: '5th Avenue',
95
+ },
96
+ }
97
+
98
+ const doc2: typeof doc1 = {
99
+ ...doc1,
100
+ age: 26,
101
+ }
102
+
103
+ expect(diff(doc1, doc2)).toEqual([{ path: ['age'], changes: doc2.age, type: 'update' }])
104
+ })
105
+
106
+ test('should correctly get updates on nested objects between two objects', () => {
107
+ const doc1 = {
108
+ name: 'John',
109
+ age: 25,
110
+ address: {
111
+ city: 'New York',
112
+ street: '5th Avenue',
113
+ },
114
+ }
115
+
116
+ const doc2: typeof doc1 = {
117
+ ...doc1,
118
+ address: {
119
+ ...doc1.address,
120
+ city: 'Los Angeles',
121
+ },
122
+ }
123
+
124
+ expect(diff(doc1, doc2)).toEqual([
125
+ {
126
+ path: ['address', 'city'],
127
+ changes: doc2.address.city,
128
+ type: 'update',
129
+ },
130
+ ])
131
+ })
132
+
133
+ test('should correctly get updates when the type is different', () => {
134
+ const doc1 = {
135
+ name: 'John',
136
+ age: 25,
137
+ address: {
138
+ city: 'New York',
139
+ street: '5th Avenue',
140
+ },
141
+ isStudent: 1,
142
+ }
143
+
144
+ const doc2 = {
145
+ ...doc1,
146
+ isStudent: true,
147
+ }
148
+
149
+ expect(diff(doc1, doc2)).toEqual([{ path: ['isStudent'], changes: doc2.isStudent, type: 'update' }])
150
+ })
151
+ })
152
+
153
+ describe('Should correctly detect `delete` type diff', () => {
154
+ test('should correctly get removed properties between two objects', () => {
155
+ const doc2 = {
156
+ name: 'John',
157
+ age: 25,
158
+ }
159
+
160
+ const doc1 = {
161
+ ...doc2,
162
+ address: {
163
+ city: 'New York',
164
+ street: '5th Avenue',
165
+ },
166
+ }
167
+
168
+ expect(diff(doc1, doc2)).toEqual([{ path: ['address'], changes: doc1.address, type: 'delete' }])
169
+ })
170
+
171
+ test('should correctly get removed properties on deeply nested objects', () => {
172
+ const doc2 = {
173
+ name: 'John',
174
+ age: 25,
175
+ address: {
176
+ city: 'New York',
177
+ street: '5th Avenue',
178
+ },
179
+ }
180
+
181
+ const doc1 = {
182
+ ...doc2,
183
+ address: {
184
+ ...doc2.address,
185
+ coordinates: {
186
+ lat: 40.7128,
187
+ long: 74.006,
188
+ },
189
+ },
190
+ }
191
+
192
+ expect(diff(doc1, doc2)).toEqual([
193
+ {
194
+ path: ['address', 'coordinates'],
195
+ changes: doc1.address.coordinates,
196
+ type: 'delete',
197
+ },
198
+ ])
199
+ })
200
+ })
201
+
202
+ describe('Should correctly detect changes on arrays', () => {
203
+ test('detect adding elements on arrays of primitives', () => {
204
+ const doc1 = {
205
+ name: 'John',
206
+ age: 25,
207
+ hobbies: ['reading', 'running'],
208
+ }
209
+
210
+ const doc2 = {
211
+ ...doc1,
212
+ hobbies: ['reading', 'running', 'swimming'],
213
+ }
214
+
215
+ expect(diff(doc1, doc2)).toEqual([{ path: ['hobbies', '2'], changes: doc2.hobbies[2], type: 'add' }])
216
+ })
217
+
218
+ test('detect adding elements on arrays of objects', () => {
219
+ const doc1 = {
220
+ name: 'John',
221
+ age: 25,
222
+ hobbies: [
223
+ { name: 'reading', duration: 2 },
224
+ { name: 'running', duration: 1 },
225
+ ],
226
+ }
227
+
228
+ const doc2 = {
229
+ ...doc1,
230
+ hobbies: [...doc1.hobbies, { name: 'swimming', duration: 3 }],
231
+ }
232
+
233
+ expect(diff(doc1, doc2)).toEqual([{ path: ['hobbies', '2'], changes: doc2.hobbies[2], type: 'add' }])
234
+ })
235
+
236
+ test('detects updates on objects on array of objects', () => {
237
+ const doc1 = {
238
+ name: 'John',
239
+ age: 25,
240
+ hobbies: [
241
+ { name: 'reading', duration: 2 },
242
+ { name: 'running', duration: 1 },
243
+ ],
244
+ }
245
+
246
+ const doc2 = {
247
+ ...doc1,
248
+ hobbies: [doc1.hobbies[0], { name: 'swimming', duration: 3 }],
249
+ }
250
+
251
+ expect(diff(doc1, doc2)).toEqual([
252
+ {
253
+ path: ['hobbies', '1', 'name'],
254
+ changes: doc2.hobbies[1]?.name,
255
+ type: 'update',
256
+ },
257
+ {
258
+ path: ['hobbies', '1', 'duration'],
259
+ changes: doc2.hobbies[1]?.duration,
260
+ type: 'update',
261
+ },
262
+ ])
263
+ })
264
+
265
+ test('detects delete operations on array of objects', () => {
266
+ const doc1 = {
267
+ name: 'John',
268
+ age: 25,
269
+ hobbies: [
270
+ { name: 'reading', duration: 2 },
271
+ { name: 'running', duration: 1 },
272
+ ],
273
+ }
274
+
275
+ const doc2 = {
276
+ ...doc1,
277
+ hobbies: [doc1.hobbies[0]],
278
+ }
279
+
280
+ expect(diff(doc1, doc2)).toEqual([{ path: ['hobbies', '1'], changes: doc1.hobbies[1], type: 'delete' }])
281
+ })
282
+ })
283
+
284
+ test('Should correctly detect multiple changes', () => {
285
+ const doc1 = {
286
+ name: 'John',
287
+ age: 25,
288
+ address: {
289
+ city: 'New York',
290
+ street: '5th Avenue',
291
+ },
292
+ hobbies: [
293
+ { name: 'reading', duration: 2 },
294
+ { name: 'running', duration: 1 },
295
+ ],
296
+ isStudent: true,
297
+ }
298
+
299
+ const doc2 = {
300
+ ...(doc1 as Partial<typeof doc1>), // Partial is needed to remove the key student key from doc2
301
+ age: 26,
302
+ address: {
303
+ ...doc1.address,
304
+ city: 'Los Angeles',
305
+ },
306
+ hobbies: [doc1.hobbies[0], { name: 'swimming', duration: 3 }, { name: 'running', duration: 2 }],
307
+ }
308
+
309
+ delete doc2.isStudent
310
+
311
+ expect(diff(doc1, doc2)).toEqual([
312
+ { path: ['age'], changes: doc2.age, type: 'update' },
313
+ { path: ['address', 'city'], changes: doc2.address.city, type: 'update' },
314
+ {
315
+ path: ['hobbies', '1', 'name'],
316
+ changes: doc2.hobbies[1]?.name,
317
+ type: 'update',
318
+ },
319
+ {
320
+ path: ['hobbies', '1', 'duration'],
321
+ changes: doc2.hobbies[1]?.duration,
322
+ type: 'update',
323
+ },
324
+ { path: ['hobbies', '2'], changes: doc2.hobbies[2], type: 'add' },
325
+ { path: ['isStudent'], changes: doc1.isStudent, type: 'delete' },
326
+ ])
327
+ })
328
+ })