@kravc/schema 2.7.6 → 2.8.0-alpha.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 (170) hide show
  1. package/README.md +19 -14
  2. package/dist/CredentialFactory.d.ts +345 -0
  3. package/dist/CredentialFactory.d.ts.map +1 -0
  4. package/dist/CredentialFactory.js +381 -0
  5. package/dist/CredentialFactory.js.map +1 -0
  6. package/dist/Schema.d.ts +448 -0
  7. package/dist/Schema.d.ts.map +1 -0
  8. package/dist/Schema.js +506 -0
  9. package/dist/Schema.js.map +1 -0
  10. package/dist/ValidationError.d.ts +70 -0
  11. package/dist/ValidationError.d.ts.map +1 -0
  12. package/dist/ValidationError.js +78 -0
  13. package/dist/ValidationError.js.map +1 -0
  14. package/dist/Validator.d.ts +483 -0
  15. package/dist/Validator.d.ts.map +1 -0
  16. package/dist/Validator.js +570 -0
  17. package/dist/Validator.js.map +1 -0
  18. package/dist/helpers/JsonSchema.d.ts +99 -0
  19. package/dist/helpers/JsonSchema.d.ts.map +1 -0
  20. package/dist/helpers/JsonSchema.js +3 -0
  21. package/dist/helpers/JsonSchema.js.map +1 -0
  22. package/dist/helpers/cleanupAttributes.d.ts +34 -0
  23. package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
  24. package/dist/helpers/cleanupAttributes.js +113 -0
  25. package/dist/helpers/cleanupAttributes.js.map +1 -0
  26. package/dist/helpers/cleanupNulls.d.ts +27 -0
  27. package/dist/helpers/cleanupNulls.d.ts.map +1 -0
  28. package/dist/helpers/cleanupNulls.js +96 -0
  29. package/dist/helpers/cleanupNulls.js.map +1 -0
  30. package/dist/helpers/getReferenceIds.d.ts +169 -0
  31. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  32. package/dist/helpers/getReferenceIds.js +241 -0
  33. package/dist/helpers/getReferenceIds.js.map +1 -0
  34. package/dist/helpers/got.d.ts +60 -0
  35. package/dist/helpers/got.d.ts.map +1 -0
  36. package/dist/helpers/got.js +72 -0
  37. package/dist/helpers/got.js.map +1 -0
  38. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  39. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  40. package/dist/helpers/mapObjectProperties.js +229 -0
  41. package/dist/helpers/mapObjectProperties.js.map +1 -0
  42. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  43. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  44. package/dist/helpers/normalizeAttributes.js +243 -0
  45. package/dist/helpers/normalizeAttributes.js.map +1 -0
  46. package/dist/helpers/normalizeProperties.d.ts +168 -0
  47. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  48. package/dist/helpers/normalizeProperties.js +223 -0
  49. package/dist/helpers/normalizeProperties.js.map +1 -0
  50. package/dist/helpers/normalizeRequired.d.ts +159 -0
  51. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  52. package/dist/helpers/normalizeRequired.js +206 -0
  53. package/dist/helpers/normalizeRequired.js.map +1 -0
  54. package/dist/helpers/normalizeType.d.ts +81 -0
  55. package/dist/helpers/normalizeType.d.ts.map +1 -0
  56. package/dist/helpers/normalizeType.js +210 -0
  57. package/dist/helpers/normalizeType.js.map +1 -0
  58. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  59. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  60. package/dist/helpers/nullifyEmptyValues.js +191 -0
  61. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  62. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  63. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  64. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  65. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  66. package/dist/helpers/validateId.d.ts +39 -0
  67. package/dist/helpers/validateId.d.ts.map +1 -0
  68. package/dist/helpers/validateId.js +51 -0
  69. package/dist/helpers/validateId.js.map +1 -0
  70. package/dist/index.d.ts +7 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/ld/documentLoader.d.ts +8 -0
  75. package/dist/ld/documentLoader.d.ts.map +1 -0
  76. package/dist/ld/documentLoader.js +24 -0
  77. package/dist/ld/documentLoader.js.map +1 -0
  78. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  79. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  80. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  81. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  82. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  83. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataContext.js +50 -0
  85. package/dist/ld/getLinkedDataContext.js.map +1 -0
  86. package/eslint.config.mjs +32 -52
  87. package/examples/credentials/createAccountCredential.ts +27 -0
  88. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  89. package/examples/index.ts +7 -0
  90. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  91. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  92. package/examples/schemas/PreferencesSchema.ts +29 -0
  93. package/examples/schemas/ProfileSchema.ts +91 -0
  94. package/examples/schemas/Status.yaml +3 -0
  95. package/examples/schemas/StatusSchema.ts +12 -0
  96. package/jest.config.mjs +5 -0
  97. package/package.json +27 -20
  98. package/src/CredentialFactory.ts +392 -0
  99. package/src/Schema.ts +583 -0
  100. package/src/ValidationError.ts +90 -0
  101. package/src/Validator.ts +603 -0
  102. package/src/__tests__/CredentialFactory.test.ts +588 -0
  103. package/src/__tests__/Schema.test.ts +371 -0
  104. package/src/__tests__/ValidationError.test.ts +235 -0
  105. package/src/__tests__/Validator.test.ts +787 -0
  106. package/src/helpers/JsonSchema.ts +119 -0
  107. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  108. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  109. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  110. package/src/helpers/__tests__/got.test.ts +193 -0
  111. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  112. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  113. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  114. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  115. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  116. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  117. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  118. package/src/helpers/__tests__/validateId.test.ts +118 -0
  119. package/src/helpers/cleanupAttributes.ts +151 -0
  120. package/src/helpers/cleanupNulls.ts +106 -0
  121. package/src/helpers/getReferenceIds.ts +273 -0
  122. package/src/helpers/got.ts +73 -0
  123. package/src/helpers/mapObjectProperties.ts +272 -0
  124. package/src/helpers/normalizeAttributes.ts +247 -0
  125. package/src/helpers/normalizeProperties.ts +249 -0
  126. package/src/helpers/normalizeRequired.ts +233 -0
  127. package/src/helpers/normalizeType.ts +235 -0
  128. package/src/helpers/nullifyEmptyValues.ts +207 -0
  129. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  130. package/src/helpers/validateId.ts +53 -0
  131. package/src/index.ts +13 -0
  132. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  133. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  134. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  135. package/src/ld/documentLoader.ts +28 -0
  136. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  137. package/src/ld/getLinkedDataContext.ts +80 -0
  138. package/tsconfig.json +27 -0
  139. package/types/credentials-context.d.ts +14 -0
  140. package/types/security-context.d.ts +6 -0
  141. package/examples/Status.yaml +0 -3
  142. package/examples/createAccountCredential.js +0 -27
  143. package/examples/createMineSweeperScoreCredential.js +0 -63
  144. package/examples/index.js +0 -9
  145. package/src/CredentialFactory.js +0 -67
  146. package/src/CredentialFactory.spec.js +0 -131
  147. package/src/Schema.js +0 -104
  148. package/src/Schema.spec.js +0 -172
  149. package/src/ValidationError.js +0 -31
  150. package/src/Validator.js +0 -128
  151. package/src/Validator.spec.js +0 -355
  152. package/src/helpers/cleanupAttributes.js +0 -71
  153. package/src/helpers/cleanupNulls.js +0 -42
  154. package/src/helpers/getReferenceIds.js +0 -71
  155. package/src/helpers/mapObject.js +0 -65
  156. package/src/helpers/normalizeAttributes.js +0 -28
  157. package/src/helpers/normalizeProperties.js +0 -61
  158. package/src/helpers/normalizeRequired.js +0 -37
  159. package/src/helpers/normalizeType.js +0 -41
  160. package/src/helpers/nullifyEmptyValues.js +0 -57
  161. package/src/helpers/removeRequiredAndDefault.js +0 -30
  162. package/src/helpers/validateId.js +0 -19
  163. package/src/index.d.ts +0 -25
  164. package/src/index.js +0 -8
  165. package/src/ld/documentLoader.js +0 -25
  166. package/src/ld/documentLoader.spec.js +0 -12
  167. package/src/ld/getLinkedDataContext.js +0 -63
  168. package/src/ld/getLinkedDataType.js +0 -38
  169. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  170. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -0,0 +1,772 @@
1
+ import cleanupNulls from '../cleanupNulls';
2
+
3
+ describe('cleanupNulls', () => {
4
+ describe('basic null removal', () => {
5
+ it('should remove null properties from a flat object', () => {
6
+ const object = {
7
+ field1: 'value1',
8
+ field2: null,
9
+ field3: 'value3',
10
+ field4: null
11
+ };
12
+
13
+ const result = cleanupNulls(object);
14
+
15
+ expect(result).toEqual({
16
+ field1: 'value1',
17
+ field3: 'value3'
18
+ });
19
+ expect(result).not.toHaveProperty('field2');
20
+ expect(result).not.toHaveProperty('field4');
21
+ });
22
+
23
+ it('should return empty object when all properties are null', () => {
24
+ const object = {
25
+ field1: null,
26
+ field2: null,
27
+ field3: null
28
+ };
29
+
30
+ const result = cleanupNulls(object);
31
+
32
+ expect(result).toEqual({});
33
+ });
34
+
35
+ it('should return unchanged object when no properties are null', () => {
36
+ const object = {
37
+ field1: 'value1',
38
+ field2: 'value2',
39
+ field3: 123,
40
+ field4: false,
41
+ field5: 0,
42
+ field6: ''
43
+ };
44
+
45
+ const result = cleanupNulls(object);
46
+
47
+ expect(result).toEqual(object);
48
+ });
49
+
50
+ it('should handle empty object', () => {
51
+ const object = {};
52
+
53
+ const result = cleanupNulls(object);
54
+
55
+ expect(result).toEqual({});
56
+ });
57
+ });
58
+
59
+ describe('nested objects', () => {
60
+ it('should remove null properties from nested objects', () => {
61
+ const object = {
62
+ field1: 'value1',
63
+ nested: {
64
+ field2: 'value2',
65
+ field3: null,
66
+ field4: 'value4'
67
+ }
68
+ };
69
+
70
+ const result = cleanupNulls(object);
71
+
72
+ expect(result).toEqual({
73
+ field1: 'value1',
74
+ nested: {
75
+ field2: 'value2',
76
+ field4: 'value4'
77
+ }
78
+ });
79
+ expect(result.nested).not.toHaveProperty('field3');
80
+ });
81
+
82
+ it('should handle deeply nested objects', () => {
83
+ const object = {
84
+ level1: {
85
+ level2: {
86
+ level3: {
87
+ field1: 'value1',
88
+ field2: null,
89
+ field3: 'value3'
90
+ }
91
+ }
92
+ }
93
+ };
94
+
95
+ const result = cleanupNulls(object);
96
+
97
+ expect(result).toEqual({
98
+ level1: {
99
+ level2: {
100
+ level3: {
101
+ field1: 'value1',
102
+ field3: 'value3'
103
+ }
104
+ }
105
+ }
106
+ });
107
+ expect((result.level1 as unknown as { level2: { level3: Record<string, unknown> } }).level2.level3).not.toHaveProperty('field2');
108
+ });
109
+
110
+ it('should remove nested object entirely if all its properties are null', () => {
111
+ const object = {
112
+ field1: 'value1',
113
+ nested: {
114
+ field2: null,
115
+ field3: null
116
+ }
117
+ };
118
+
119
+ const result = cleanupNulls(object);
120
+
121
+ expect(result).toEqual({
122
+ field1: 'value1',
123
+ nested: {}
124
+ });
125
+ });
126
+
127
+ it('should handle multiple nested objects', () => {
128
+ const object = {
129
+ nested1: {
130
+ field1: 'value1',
131
+ field2: null
132
+ },
133
+ nested2: {
134
+ field3: null,
135
+ field4: 'value4'
136
+ }
137
+ };
138
+
139
+ const result = cleanupNulls(object);
140
+
141
+ expect(result).toEqual({
142
+ nested1: {
143
+ field1: 'value1'
144
+ },
145
+ nested2: {
146
+ field4: 'value4'
147
+ }
148
+ });
149
+ });
150
+ });
151
+
152
+ describe('arrays', () => {
153
+ it('should handle arrays of objects with null properties', () => {
154
+ const object = {
155
+ arrayField: [
156
+ { field1: 'value1', field2: null },
157
+ { field3: null, field4: 'value4' },
158
+ { field5: 'value5', field6: 'value6' }
159
+ ]
160
+ };
161
+
162
+ const result = cleanupNulls(object);
163
+
164
+ expect(result.arrayField).toHaveLength(3);
165
+ expect(result.arrayField[0]).toEqual({ field1: 'value1' });
166
+ expect(result.arrayField[0]).not.toHaveProperty('field2');
167
+ expect(result.arrayField[1]).toEqual({ field4: 'value4' });
168
+ expect(result.arrayField[1]).not.toHaveProperty('field3');
169
+ expect(result.arrayField[2]).toEqual({ field5: 'value5', field6: 'value6' });
170
+ });
171
+
172
+ it('should handle nested objects within array items', () => {
173
+ const object = {
174
+ arrayField: [
175
+ {
176
+ nested: {
177
+ field1: 'value1',
178
+ field2: null
179
+ }
180
+ }
181
+ ]
182
+ };
183
+
184
+ const result = cleanupNulls(object);
185
+
186
+ expect(result.arrayField[0].nested).toEqual({ field1: 'value1' });
187
+ expect((result.arrayField[0] as unknown as { nested: Record<string, unknown> }).nested).not.toHaveProperty('field2');
188
+ });
189
+
190
+ it('should handle empty arrays', () => {
191
+ const object = {
192
+ arrayField: []
193
+ };
194
+
195
+ const result = cleanupNulls(object);
196
+
197
+ expect(result.arrayField).toEqual([]);
198
+ });
199
+
200
+ it('should handle arrays with empty objects', () => {
201
+ const object = {
202
+ arrayField: [
203
+ {},
204
+ { field1: null },
205
+ { field2: 'value2' }
206
+ ]
207
+ };
208
+
209
+ const result = cleanupNulls(object);
210
+
211
+ expect(result.arrayField).toHaveLength(3);
212
+ expect(result.arrayField[0]).toEqual({});
213
+ expect(result.arrayField[1]).toEqual({});
214
+ expect(result.arrayField[2]).toEqual({ field2: 'value2' });
215
+ });
216
+ });
217
+
218
+ describe('complex nested scenarios', () => {
219
+ it('should handle object with mix of nested objects and arrays', () => {
220
+ const object = {
221
+ field1: 'value1',
222
+ field2: null,
223
+ nested: {
224
+ field3: 'value3',
225
+ field4: null,
226
+ arrayField: [
227
+ { field5: 'value5', field6: null },
228
+ { field7: null, field8: 'value8' }
229
+ ]
230
+ }
231
+ };
232
+
233
+ const result = cleanupNulls(object);
234
+
235
+ expect(result).toEqual({
236
+ field1: 'value1',
237
+ nested: {
238
+ field3: 'value3',
239
+ arrayField: [
240
+ { field5: 'value5' },
241
+ { field8: 'value8' }
242
+ ]
243
+ }
244
+ });
245
+ expect(result).not.toHaveProperty('field2');
246
+ expect((result.nested as unknown as Record<string, unknown>)).not.toHaveProperty('field4');
247
+ });
248
+
249
+ it('should handle deeply nested arrays of objects', () => {
250
+ const object = {
251
+ level1: {
252
+ level2: {
253
+ arrayField: [
254
+ {
255
+ nested: {
256
+ field1: 'value1',
257
+ field2: null
258
+ }
259
+ }
260
+ ]
261
+ }
262
+ }
263
+ };
264
+
265
+ const result = cleanupNulls(object);
266
+
267
+ expect((result.level1 as unknown as { level2: { arrayField: Array<{ nested: Record<string, unknown> }> } }).level2.arrayField[0].nested).toEqual({ field1: 'value1' });
268
+ expect((result.level1 as unknown as { level2: { arrayField: Array<{ nested: Record<string, unknown> }> } }).level2.arrayField[0].nested).not.toHaveProperty('field2');
269
+ });
270
+
271
+ it('should handle multiple arrays with nested structures', () => {
272
+ const object = {
273
+ array1: [
274
+ { field1: 'value1', field2: null },
275
+ { nested: { field3: null, field4: 'value4' } }
276
+ ],
277
+ array2: [
278
+ { field5: null, field6: 'value6' }
279
+ ]
280
+ };
281
+
282
+ const result = cleanupNulls(object);
283
+
284
+ expect(result.array1[0]).toEqual({ field1: 'value1' });
285
+ expect((result.array1[1] as unknown as { nested: Record<string, unknown> }).nested).toEqual({ field4: 'value4' });
286
+ expect(result.array2[0]).toEqual({ field6: 'value6' });
287
+ });
288
+ });
289
+
290
+ describe('immutability', () => {
291
+ it('should not modify the original object', () => {
292
+ const object = {
293
+ field1: 'value1',
294
+ field2: null,
295
+ nested: {
296
+ field3: 'value3',
297
+ field4: null
298
+ }
299
+ };
300
+ const originalClone = JSON.parse(JSON.stringify(object));
301
+
302
+ const result = cleanupNulls(object);
303
+
304
+ expect(object).toEqual(originalClone);
305
+ expect(object).toHaveProperty('field2');
306
+ expect((object.nested as unknown as Record<string, unknown>)).toHaveProperty('field4');
307
+ expect(result).not.toHaveProperty('field2');
308
+ });
309
+
310
+ it('should return a deep clone', () => {
311
+ const object = {
312
+ field1: 'value1',
313
+ nested: {
314
+ field2: 'value2'
315
+ }
316
+ };
317
+
318
+ const result = cleanupNulls(object);
319
+
320
+ expect(result).not.toBe(object);
321
+ expect(result.nested).not.toBe(object.nested);
322
+ });
323
+ });
324
+
325
+ describe('edge cases', () => {
326
+ it('should handle object with only null values at different nesting levels', () => {
327
+ const object = {
328
+ field1: null,
329
+ nested: {
330
+ field2: null,
331
+ deeper: {
332
+ field3: null
333
+ }
334
+ }
335
+ };
336
+
337
+ const result = cleanupNulls(object);
338
+
339
+ expect(result).toEqual({
340
+ nested: {
341
+ deeper: {}
342
+ }
343
+ });
344
+ });
345
+
346
+ it('should handle object with falsy but non-null values', () => {
347
+ const object = {
348
+ field1: false,
349
+ field2: 0,
350
+ field3: '',
351
+ field4: null,
352
+ field5: undefined
353
+ };
354
+
355
+ const result = cleanupNulls(object);
356
+
357
+ expect(result).toEqual({
358
+ field1: false,
359
+ field2: 0,
360
+ field3: '',
361
+ field5: undefined
362
+ });
363
+ expect(result).not.toHaveProperty('field4');
364
+ });
365
+
366
+ it('should handle array with null values (not objects)', () => {
367
+ const object = {
368
+ arrayField: [null, 'value1', null, 'value2']
369
+ };
370
+
371
+ // Note: The current implementation doesn't remove null from arrays,
372
+ // it only processes array items if they are objects
373
+ const result = cleanupNulls(object);
374
+
375
+ // Arrays with primitive nulls are not modified by the current implementation
376
+ expect(result.arrayField).toEqual([null, 'value1', null, 'value2']);
377
+ });
378
+
379
+ it('should handle nested arrays (arrays containing arrays)', () => {
380
+ const object = {
381
+ nestedArrays: [
382
+ [
383
+ { field1: 'value1', field2: null },
384
+ { field3: null, field4: 'value4' }
385
+ ],
386
+ [
387
+ { field5: 'value5', field6: null }
388
+ ]
389
+ ]
390
+ };
391
+
392
+ const result = cleanupNulls(object);
393
+
394
+ expect(result.nestedArrays[0][0]).toEqual({ field1: 'value1' });
395
+ expect(result.nestedArrays[0][0]).not.toHaveProperty('field2');
396
+ expect(result.nestedArrays[0][1]).toEqual({ field4: 'value4' });
397
+ expect(result.nestedArrays[0][1]).not.toHaveProperty('field3');
398
+ expect(result.nestedArrays[1][0]).toEqual({ field5: 'value5' });
399
+ expect(result.nestedArrays[1][0]).not.toHaveProperty('field6');
400
+ });
401
+
402
+ it('should handle arrays of arrays of objects with nulls', () => {
403
+ const object = {
404
+ deepArrays: [
405
+ [
406
+ [
407
+ { field1: 'value1', field2: null }
408
+ ]
409
+ ]
410
+ ]
411
+ };
412
+
413
+ const result = cleanupNulls(object);
414
+
415
+ expect((result.deepArrays[0][0][0] as Record<string, unknown>)).toEqual({ field1: 'value1' });
416
+ expect((result.deepArrays[0][0][0] as Record<string, unknown>)).not.toHaveProperty('field2');
417
+ });
418
+
419
+ it('should handle mixed arrays (objects and primitives)', () => {
420
+ const object = {
421
+ mixedArray: [
422
+ { field1: 'value1', field2: null },
423
+ 'primitive',
424
+ 123,
425
+ null,
426
+ { field3: null, field4: 'value4' },
427
+ false,
428
+ []
429
+ ]
430
+ };
431
+
432
+ const result = cleanupNulls(object);
433
+
434
+ expect(result.mixedArray[0]).toEqual({ field1: 'value1' });
435
+ expect(result.mixedArray[0]).not.toHaveProperty('field2');
436
+ expect(result.mixedArray[1]).toBe('primitive');
437
+ expect(result.mixedArray[2]).toBe(123);
438
+ expect(result.mixedArray[3]).toBe(null); // Primitive nulls in arrays are preserved
439
+ expect(result.mixedArray[4]).toEqual({ field4: 'value4' });
440
+ expect(result.mixedArray[4]).not.toHaveProperty('field3');
441
+ expect(result.mixedArray[5]).toBe(false);
442
+ expect(result.mixedArray[6]).toEqual([]);
443
+ });
444
+
445
+ it('should handle objects with numeric string keys', () => {
446
+ const object = {
447
+ '0': 'value0',
448
+ '1': null,
449
+ '2': 'value2',
450
+ normalKey: 'value',
451
+ anotherNull: null
452
+ };
453
+
454
+ const result = cleanupNulls(object);
455
+
456
+ expect(result).toEqual({
457
+ '0': 'value0',
458
+ '2': 'value2',
459
+ normalKey: 'value'
460
+ });
461
+ expect(result).not.toHaveProperty('1');
462
+ expect(result).not.toHaveProperty('anotherNull');
463
+ });
464
+
465
+ it('should handle objects with special property names', () => {
466
+ const object = {
467
+ 'has-dash': 'value1',
468
+ 'has_underscore': null,
469
+ 'has.dot': 'value3',
470
+ 'has space': null,
471
+ '123numeric': 'value5',
472
+ normalKey: null
473
+ };
474
+
475
+ const result = cleanupNulls(object);
476
+
477
+ expect(result).toEqual({
478
+ 'has-dash': 'value1',
479
+ 'has.dot': 'value3',
480
+ '123numeric': 'value5'
481
+ });
482
+ expect(result).not.toHaveProperty('has_underscore');
483
+ expect(result).not.toHaveProperty('has space');
484
+ expect(result).not.toHaveProperty('normalKey');
485
+ });
486
+
487
+ it('should handle sparse arrays', () => {
488
+ const sparseArray: unknown[] = [];
489
+ sparseArray[0] = { field1: 'value1', field2: null };
490
+ sparseArray[5] = { field3: null, field4: 'value4' };
491
+ sparseArray[10] = null;
492
+
493
+ const object = {
494
+ sparseArray
495
+ };
496
+
497
+ const result = cleanupNulls(object);
498
+
499
+ expect(result.sparseArray[0]).toEqual({ field1: 'value1' });
500
+ expect((result.sparseArray[0] as Record<string, unknown>)).not.toHaveProperty('field2');
501
+ expect(result.sparseArray[5]).toEqual({ field4: 'value4' });
502
+ expect((result.sparseArray[5] as Record<string, unknown>)).not.toHaveProperty('field3');
503
+ expect(result.sparseArray[10]).toBe(null); // Primitive nulls preserved
504
+ });
505
+
506
+ it('should handle Date objects (treated as regular objects)', () => {
507
+ const date1 = new Date('2023-01-01');
508
+ const date2 = new Date('2023-02-01');
509
+ const object = {
510
+ date1,
511
+ date2,
512
+ nullDate: null,
513
+ nested: {
514
+ date3: date1,
515
+ nullField: null
516
+ }
517
+ };
518
+
519
+ const result = cleanupNulls(object);
520
+
521
+ expect(result.date1).toBeInstanceOf(Date);
522
+ expect(result.date2).toBeInstanceOf(Date);
523
+ expect(result).not.toHaveProperty('nullDate');
524
+ expect((result.nested as Record<string, unknown>).date3).toBeInstanceOf(Date);
525
+ expect(result.nested).not.toHaveProperty('nullField');
526
+ });
527
+
528
+ it('should handle RegExp objects (treated as regular objects)', () => {
529
+ const regex1 = /test/gi;
530
+ const regex2 = /pattern/;
531
+ const object = {
532
+ regex1,
533
+ regex2,
534
+ nullRegex: null,
535
+ nested: {
536
+ regex3: regex1,
537
+ nullField: null
538
+ }
539
+ };
540
+
541
+ const result = cleanupNulls(object);
542
+
543
+ expect(result.regex1).toBeInstanceOf(RegExp);
544
+ expect(result.regex2).toBeInstanceOf(RegExp);
545
+ expect(result).not.toHaveProperty('nullRegex');
546
+ expect((result.nested as Record<string, unknown>).regex3).toBeInstanceOf(RegExp);
547
+ expect(result.nested).not.toHaveProperty('nullField');
548
+ });
549
+
550
+ it('should handle arrays containing Date and RegExp objects', () => {
551
+ const date = new Date('2023-01-01');
552
+ const regex = /test/;
553
+ const object = {
554
+ mixedTypes: [
555
+ { field1: 'value1', field2: null },
556
+ date,
557
+ regex,
558
+ { field3: null, field4: 'value4' },
559
+ null
560
+ ]
561
+ };
562
+
563
+ const result = cleanupNulls(object);
564
+
565
+ expect(result.mixedTypes[0]).toEqual({ field1: 'value1' });
566
+ expect((result.mixedTypes[0] as Record<string, unknown>)).not.toHaveProperty('field2');
567
+ expect(result.mixedTypes[1]).toBeInstanceOf(Date);
568
+ expect(result.mixedTypes[2]).toBeInstanceOf(RegExp);
569
+ expect(result.mixedTypes[3]).toEqual({ field4: 'value4' });
570
+ expect((result.mixedTypes[3] as Record<string, unknown>)).not.toHaveProperty('field3');
571
+ expect(result.mixedTypes[4]).toBe(null);
572
+ });
573
+
574
+ it('should handle very deeply nested structures', () => {
575
+ let deepObject: Record<string, unknown> = { field: 'value' };
576
+ for (let i = 0; i < 10; i++) {
577
+ const newLevel: Record<string, unknown> = {
578
+ nested: deepObject,
579
+ nullField: null,
580
+ value: `level${i}`
581
+ };
582
+ deepObject = newLevel;
583
+ }
584
+
585
+ const result = cleanupNulls(deepObject);
586
+
587
+ // Verify structure is preserved and nulls are removed
588
+ let current: Record<string, unknown> = result;
589
+ for (let i = 9; i >= 0; i--) {
590
+ expect(current).toHaveProperty('value', `level${i}`);
591
+ expect(current).not.toHaveProperty('nullField');
592
+ expect(current).toHaveProperty('nested');
593
+ current = current.nested as Record<string, unknown>;
594
+ }
595
+ // After all nested levels, we should reach the innermost object
596
+ expect(current).toHaveProperty('field', 'value');
597
+ });
598
+
599
+ it('should handle objects with empty string keys', () => {
600
+ const object: Record<string, unknown> = {
601
+ '': 'emptyKeyValue',
602
+ ' ': 'spaceKeyValue',
603
+ normalKey: null,
604
+ anotherKey: 'value'
605
+ };
606
+
607
+ const result = cleanupNulls(object);
608
+
609
+ expect(result['']).toBe('emptyKeyValue');
610
+ expect(result[' ']).toBe('spaceKeyValue');
611
+ expect(result).not.toHaveProperty('normalKey');
612
+ expect(result.anotherKey).toBe('value');
613
+ });
614
+
615
+ it('should handle objects with very long property names', () => {
616
+ const longKey = 'a'.repeat(1000);
617
+ const object: Record<string, unknown> = {
618
+ [longKey]: 'longKeyValue',
619
+ normalKey: null,
620
+ shortKey: 'value'
621
+ };
622
+
623
+ const result = cleanupNulls(object);
624
+
625
+ expect(result[longKey]).toBe('longKeyValue');
626
+ expect(result).not.toHaveProperty('normalKey');
627
+ expect(result.shortKey).toBe('value');
628
+ });
629
+
630
+ it('should handle objects with unicode property names', () => {
631
+ const object: Record<string, unknown> = {
632
+ 'café': 'value1',
633
+ '日本語': null,
634
+ '🚀': 'emojiValue',
635
+ 'русский': null,
636
+ normalKey: 'value'
637
+ };
638
+
639
+ const result = cleanupNulls(object);
640
+
641
+ expect(result['café']).toBe('value1');
642
+ expect(result).not.toHaveProperty('日本語');
643
+ expect(result['🚀']).toBe('emojiValue');
644
+ expect(result).not.toHaveProperty('русский');
645
+ expect(result.normalKey).toBe('value');
646
+ });
647
+
648
+ it('should handle arrays with mixed object types', () => {
649
+ const date = new Date();
650
+ const regex = /test/;
651
+ const object = {
652
+ mixedObjects: [
653
+ { field1: 'value1', field2: null },
654
+ { field3: null, nested: { field4: 'value4', field5: null } },
655
+ date,
656
+ regex,
657
+ { field6: 'value6', field7: null, date, regex }
658
+ ]
659
+ };
660
+
661
+ const result = cleanupNulls(object);
662
+
663
+ expect(result.mixedObjects[0]).toEqual({ field1: 'value1' });
664
+ expect((result.mixedObjects[0] as Record<string, unknown>)).not.toHaveProperty('field2');
665
+ expect(result.mixedObjects[1]).toEqual({ nested: { field4: 'value4' } });
666
+ expect((result.mixedObjects[1] as Record<string, unknown>).nested).not.toHaveProperty('field5');
667
+ expect(result.mixedObjects[2]).toBeInstanceOf(Date);
668
+ expect(result.mixedObjects[3]).toBeInstanceOf(RegExp);
669
+ expect((result.mixedObjects[4] as Record<string, unknown>).field6).toBe('value6');
670
+ expect((result.mixedObjects[4] as Record<string, unknown>)).not.toHaveProperty('field7');
671
+ expect((result.mixedObjects[4] as Record<string, unknown>).date).toBeInstanceOf(Date);
672
+ expect((result.mixedObjects[4] as Record<string, unknown>).regex).toBeInstanceOf(RegExp);
673
+ });
674
+
675
+ it('should handle objects containing arrays with various primitive types', () => {
676
+ const object = {
677
+ primitives: {
678
+ strings: ['a', null, 'b'],
679
+ numbers: [1, null, 2],
680
+ booleans: [true, null, false],
681
+ nulls: [null, null, null],
682
+ mixed: ['string', 123, true, null, false, 0, '']
683
+ },
684
+ nullField: null
685
+ };
686
+
687
+ const result = cleanupNulls(object);
688
+
689
+ // Primitive nulls in arrays are preserved
690
+ expect(result.primitives.strings).toEqual(['a', null, 'b']);
691
+ expect(result.primitives.numbers).toEqual([1, null, 2]);
692
+ expect(result.primitives.booleans).toEqual([true, null, false]);
693
+ expect(result.primitives.nulls).toEqual([null, null, null]);
694
+ expect(result.primitives.mixed).toEqual(['string', 123, true, null, false, 0, '']);
695
+ expect(result).not.toHaveProperty('nullField');
696
+ });
697
+
698
+ it('should handle complex real-world structure (like API response)', () => {
699
+ const object = {
700
+ id: '123',
701
+ name: 'John Doe',
702
+ email: null,
703
+ profile: {
704
+ age: 30,
705
+ avatar: null,
706
+ preferences: {
707
+ theme: 'dark',
708
+ notifications: null
709
+ }
710
+ },
711
+ addresses: [
712
+ {
713
+ street: '123 Main St',
714
+ zip: null,
715
+ city: 'New York',
716
+ coordinates: {
717
+ lat: 40.7128,
718
+ lng: null
719
+ }
720
+ },
721
+ {
722
+ street: null,
723
+ zip: '10001',
724
+ city: 'New York'
725
+ }
726
+ ],
727
+ metadata: {
728
+ createdAt: new Date('2023-01-01'),
729
+ updatedAt: null,
730
+ tags: ['important', null, 'user']
731
+ }
732
+ };
733
+
734
+ const result = cleanupNulls(object);
735
+
736
+ expect(result).toEqual({
737
+ id: '123',
738
+ name: 'John Doe',
739
+ profile: {
740
+ age: 30,
741
+ preferences: {
742
+ theme: 'dark'
743
+ }
744
+ },
745
+ addresses: [
746
+ {
747
+ street: '123 Main St',
748
+ city: 'New York',
749
+ coordinates: {
750
+ lat: 40.7128
751
+ }
752
+ },
753
+ {
754
+ zip: '10001',
755
+ city: 'New York'
756
+ }
757
+ ],
758
+ metadata: {
759
+ createdAt: expect.any(Date),
760
+ tags: ['important', null, 'user'] // Primitive nulls in arrays preserved
761
+ }
762
+ });
763
+ expect(result).not.toHaveProperty('email');
764
+ expect((result.profile as Record<string, unknown>)).not.toHaveProperty('avatar');
765
+ expect((result.profile as Record<string, unknown>).preferences).not.toHaveProperty('notifications');
766
+ expect((result.addresses[0] as Record<string, unknown>)).not.toHaveProperty('zip');
767
+ expect((result.addresses[0] as Record<string, unknown>).coordinates).not.toHaveProperty('lng');
768
+ expect((result.addresses[1] as Record<string, unknown>)).not.toHaveProperty('street');
769
+ expect((result.metadata as Record<string, unknown>)).not.toHaveProperty('updatedAt');
770
+ });
771
+ });
772
+ });