@synnaxlabs/x 0.53.1 → 0.54.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 (119) hide show
  1. package/.turbo/turbo-build.log +14 -8
  2. package/dist/src/array/nullable.d.ts +10 -1
  3. package/dist/src/array/nullable.d.ts.map +1 -1
  4. package/dist/src/binary/codec.d.ts +5 -5
  5. package/dist/src/binary/codec.d.ts.map +1 -1
  6. package/dist/src/caseconv/caseconv.bench.d.ts +2 -0
  7. package/dist/src/caseconv/caseconv.bench.d.ts.map +1 -0
  8. package/dist/src/caseconv/caseconv.d.ts +19 -1
  9. package/dist/src/caseconv/caseconv.d.ts.map +1 -1
  10. package/dist/src/color/color.d.ts +22 -24
  11. package/dist/src/color/color.d.ts.map +1 -1
  12. package/dist/src/color/gradient.d.ts +10 -10
  13. package/dist/src/color/palette.d.ts +16 -16
  14. package/dist/src/control/control.d.ts +21 -37
  15. package/dist/src/control/control.d.ts.map +1 -1
  16. package/dist/src/control/external.d.ts +3 -0
  17. package/dist/src/control/external.d.ts.map +1 -0
  18. package/dist/src/control/index.d.ts +1 -1
  19. package/dist/src/control/index.d.ts.map +1 -1
  20. package/dist/src/control/types.gen.d.ts +34 -0
  21. package/dist/src/control/types.gen.d.ts.map +1 -0
  22. package/dist/src/deep/merge.d.ts.map +1 -1
  23. package/dist/src/label/index.d.ts +1 -1
  24. package/dist/src/label/index.d.ts.map +1 -1
  25. package/dist/src/label/types.gen.d.ts +51 -0
  26. package/dist/src/label/types.gen.d.ts.map +1 -0
  27. package/dist/src/math/constants.d.ts +1 -0
  28. package/dist/src/math/constants.d.ts.map +1 -1
  29. package/dist/src/record/record.d.ts +12 -12
  30. package/dist/src/record/record.d.ts.map +1 -1
  31. package/dist/src/spatial/base.d.ts +40 -43
  32. package/dist/src/spatial/base.d.ts.map +1 -1
  33. package/dist/src/spatial/bounds/bounds.d.ts +2 -2
  34. package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
  35. package/dist/src/spatial/box/box.d.ts +4 -16
  36. package/dist/src/spatial/box/box.d.ts.map +1 -1
  37. package/dist/src/spatial/dimensions/dimensions.d.ts +3 -7
  38. package/dist/src/spatial/dimensions/dimensions.d.ts.map +1 -1
  39. package/dist/src/spatial/direction/direction.d.ts +3 -3
  40. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  41. package/dist/src/spatial/external.d.ts +1 -0
  42. package/dist/src/spatial/external.d.ts.map +1 -1
  43. package/dist/src/spatial/location/location.d.ts +9 -3
  44. package/dist/src/spatial/location/location.d.ts.map +1 -1
  45. package/dist/src/spatial/spatial.d.ts +2 -1
  46. package/dist/src/spatial/spatial.d.ts.map +1 -1
  47. package/dist/src/spatial/types.gen.d.ts +20 -0
  48. package/dist/src/spatial/types.gen.d.ts.map +1 -0
  49. package/dist/src/spatial/xy/xy.d.ts +3 -2
  50. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  51. package/dist/src/status/external.d.ts +3 -0
  52. package/dist/src/status/external.d.ts.map +1 -0
  53. package/dist/src/status/index.d.ts +1 -1
  54. package/dist/src/status/index.d.ts.map +1 -1
  55. package/dist/src/status/status.d.ts +5 -35
  56. package/dist/src/status/status.d.ts.map +1 -1
  57. package/dist/src/status/types.gen.d.ts +74 -0
  58. package/dist/src/status/types.gen.d.ts.map +1 -0
  59. package/dist/src/telem/external.d.ts +4 -0
  60. package/dist/src/telem/external.d.ts.map +1 -0
  61. package/dist/src/telem/index.d.ts +2 -3
  62. package/dist/src/telem/index.d.ts.map +1 -1
  63. package/dist/src/telem/telem.d.ts +62 -0
  64. package/dist/src/telem/telem.d.ts.map +1 -1
  65. package/dist/src/zod/nullToUndefined.d.ts +1 -1
  66. package/dist/src/zod/nullToUndefined.d.ts.map +1 -1
  67. package/dist/src/zod/schemas.d.ts +6 -0
  68. package/dist/src/zod/schemas.d.ts.map +1 -1
  69. package/dist/x.cjs +8 -13
  70. package/dist/x.js +4393 -5591
  71. package/package.json +10 -10
  72. package/src/array/nullable.ts +10 -1
  73. package/src/binary/codec.spec.ts +31 -0
  74. package/src/binary/codec.ts +9 -8
  75. package/src/caseconv/caseconv.bench.ts +270 -0
  76. package/src/caseconv/caseconv.spec.ts +534 -0
  77. package/src/caseconv/caseconv.ts +186 -41
  78. package/src/color/color.spec.ts +51 -36
  79. package/src/color/color.ts +7 -8
  80. package/src/control/control.ts +7 -32
  81. package/src/{label/label.ts → control/external.ts} +2 -13
  82. package/src/control/index.ts +1 -1
  83. package/src/control/types.gen.ts +52 -0
  84. package/src/deep/merge.ts +2 -1
  85. package/src/deep/path.ts +1 -1
  86. package/src/deep/remove.ts +1 -1
  87. package/src/label/index.ts +1 -1
  88. package/src/label/types.gen.ts +35 -0
  89. package/src/math/constants.ts +1 -0
  90. package/src/migrate/migrate.ts +2 -2
  91. package/src/record/record.spec.ts +31 -7
  92. package/src/record/record.ts +17 -17
  93. package/src/spatial/base.ts +63 -39
  94. package/src/spatial/bounds/bounds.ts +2 -2
  95. package/src/spatial/box/box.ts +2 -2
  96. package/src/spatial/dimensions/dimensions.ts +11 -5
  97. package/src/spatial/direction/direction.ts +2 -2
  98. package/src/spatial/external.ts +1 -0
  99. package/src/spatial/location/location.ts +15 -13
  100. package/src/spatial/spatial.ts +29 -2
  101. package/src/spatial/sticky/sticky.ts +1 -1
  102. package/src/spatial/types.gen.ts +28 -0
  103. package/src/spatial/xy/xy.ts +9 -10
  104. package/src/status/external.ts +11 -0
  105. package/src/status/index.ts +1 -1
  106. package/src/status/status.ts +11 -59
  107. package/src/status/types.gen.ts +118 -0
  108. package/src/strings/deduplicateFileName.ts +3 -3
  109. package/src/telem/external.ts +12 -0
  110. package/src/telem/index.ts +2 -3
  111. package/src/telem/telem.ts +30 -5
  112. package/src/zod/nullToUndefined.ts +4 -4
  113. package/src/zod/schemas.ts +9 -2
  114. package/src/zod/util.ts +2 -2
  115. package/tsconfig.json +3 -2
  116. package/tsconfig.tsbuildinfo +1 -1
  117. package/vite.config.ts +8 -1
  118. package/dist/src/label/label.d.ts +0 -25
  119. package/dist/src/label/label.d.ts.map +0 -1
@@ -8,8 +8,14 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { describe, expect, it } from "vitest";
11
+ import { z } from "zod";
11
12
 
12
13
  import { caseconv } from "@/caseconv";
14
+ import { record } from "@/record";
15
+
16
+ // Recursive record type for accessing case-converted results in tests.
17
+ // Case conversion changes key names, so we can't use the original type.
18
+ type R = Record<string, any>;
13
19
 
14
20
  describe("caseconv", () => {
15
21
  describe("snakeToCamel", () => {
@@ -159,4 +165,532 @@ describe("caseconv", () => {
159
165
  });
160
166
  });
161
167
  });
168
+
169
+ describe("preserveCase", () => {
170
+ describe("with ZodRecord", () => {
171
+ it("should preserve case for record keys marked with preserveCase", () => {
172
+ const schema = z.object({
173
+ read: z.object({
174
+ index: z.number(),
175
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
176
+ }),
177
+ });
178
+
179
+ const input = {
180
+ read: {
181
+ index: 0,
182
+ channels: {
183
+ "ns=2;s=Temperature": 123,
184
+ "i=2258": 456,
185
+ holding_register_input: 789,
186
+ },
187
+ },
188
+ };
189
+
190
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
191
+
192
+ expect(result.read).toBeDefined();
193
+ expect(result.read.index).toBe(0);
194
+ expect(result.read.channels["ns=2;s=Temperature"]).toBe(123);
195
+ expect(result.read.channels["i=2258"]).toBe(456);
196
+ expect(result.read.channels.holding_register_input).toBe(789);
197
+ expect(result.read.channels.holdingRegisterInput).toBeUndefined();
198
+ });
199
+
200
+ it("should preserve case for nested objects in marked records", () => {
201
+ const schema = z.object({
202
+ data: caseconv.preserveCase(
203
+ z.record(z.string(), z.object({ value_name: z.number() })),
204
+ ),
205
+ });
206
+
207
+ const input = {
208
+ data: {
209
+ "ns=2;s=Test": { value_name: 42 },
210
+ },
211
+ };
212
+
213
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
214
+
215
+ expect(result.data["ns=2;s=Test"]).toBeDefined();
216
+ expect(result.data["ns=2;s=Test"].value_name).toBe(42);
217
+ expect(result.data["ns=2;s=Test"].valueName).toBeUndefined();
218
+ });
219
+ });
220
+
221
+ describe("with ZodObject", () => {
222
+ it("should preserve case for nested properties marked with preserveCase", () => {
223
+ const schema = z.object({
224
+ normalProp: z.string(),
225
+ preservedProp: caseconv.preserveCase(
226
+ z.object({
227
+ nested_key: z.number(),
228
+ another_nested: z.string(),
229
+ }),
230
+ ),
231
+ });
232
+
233
+ const input = {
234
+ normal_prop: "test",
235
+ preserved_prop: {
236
+ nested_key: 123,
237
+ another_nested: "value",
238
+ },
239
+ };
240
+
241
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
242
+
243
+ expect(result.normalProp).toBe("test");
244
+ expect(result.normal_prop).toBeUndefined();
245
+ expect(result.preservedProp.nested_key).toBe(123);
246
+ expect(result.preservedProp.another_nested).toBe("value");
247
+ expect(result.preservedProp.nestedKey).toBeUndefined();
248
+ expect(result.preservedProp.anotherNested).toBeUndefined();
249
+ });
250
+ });
251
+
252
+ describe("without schema", () => {
253
+ it("should convert normally when no schema is provided", () => {
254
+ const input = {
255
+ read: {
256
+ channels: {
257
+ "ns=2;s=Temperature": 123,
258
+ holding_register_input: 456,
259
+ },
260
+ },
261
+ };
262
+
263
+ const result = caseconv.snakeToCamel(input) as R;
264
+
265
+ expect(result.read.channels["ns=2;s=Temperature"]).toBe(123);
266
+ expect(result.read.channels.holdingRegisterInput).toBe(456);
267
+ expect(result.read.channels.holding_register_input).toBeUndefined();
268
+ });
269
+ });
270
+
271
+ describe("camelToSnake with preserveCase", () => {
272
+ it("should preserve case when encoding with camelToSnake", () => {
273
+ const schema = z.object({
274
+ read: z.object({
275
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
276
+ }),
277
+ });
278
+
279
+ const input = {
280
+ read: {
281
+ channels: {
282
+ "ns=2;s=Temperature": 123,
283
+ "i=2258": 456,
284
+ },
285
+ },
286
+ };
287
+
288
+ const result = caseconv.camelToSnake(input, { schema });
289
+
290
+ // Record keys should NOT be converted
291
+ expect(result.read.channels["ns=2;s=Temperature"]).toBe(123);
292
+ expect(result.read.channels["i=2258"]).toBe(456);
293
+ });
294
+ });
295
+
296
+ describe("comprehensive edge cases", () => {
297
+ it("should handle empty preserved records", () => {
298
+ const schema = z.object({
299
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
300
+ });
301
+
302
+ const result = caseconv.snakeToCamel({ channels: {} }, { schema });
303
+ expect(result.channels).toEqual({});
304
+ });
305
+
306
+ it("should handle arrays within preserved records", () => {
307
+ const schema = z.object({
308
+ channels: caseconv.preserveCase(z.record(z.string(), z.array(z.number()))),
309
+ });
310
+
311
+ const input = {
312
+ channels: {
313
+ "ns=2;s=ArrayChannel": [1, 2, 3],
314
+ },
315
+ };
316
+
317
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
318
+ expect(result.channels["ns=2;s=ArrayChannel"]).toEqual([1, 2, 3]);
319
+ });
320
+
321
+ it("should handle deeply nested preserved objects", () => {
322
+ const schema = z.object({
323
+ outer: z.object({
324
+ inner: caseconv.preserveCase(
325
+ z.object({
326
+ deep_value: z.number(),
327
+ }),
328
+ ),
329
+ }),
330
+ });
331
+
332
+ const input = {
333
+ outer: {
334
+ inner: {
335
+ deep_value: 42,
336
+ },
337
+ },
338
+ };
339
+
340
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
341
+ expect(result.outer.inner.deep_value).toBe(42);
342
+ expect(result.outer.inner.deepValue).toBeUndefined();
343
+ });
344
+
345
+ it("should handle multiple preserved records at different levels", () => {
346
+ const schema = z.object({
347
+ read: z.object({
348
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
349
+ }),
350
+ write: z.object({
351
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
352
+ }),
353
+ });
354
+
355
+ const input = {
356
+ read: {
357
+ channels: {
358
+ holding_register_input: 123,
359
+ },
360
+ },
361
+ write: {
362
+ channels: {
363
+ coil_output: 456,
364
+ },
365
+ },
366
+ };
367
+
368
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
369
+ expect(result.read.channels.holding_register_input).toBe(123);
370
+ expect(result.write.channels.coil_output).toBe(456);
371
+ });
372
+
373
+ it("should handle Modbus channel keys with hyphens and underscores", () => {
374
+ const schema = z.object({
375
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
376
+ });
377
+
378
+ const input = {
379
+ channels: {
380
+ "holding_register_input-100-float32": 123,
381
+ "register_input-200": 456,
382
+ "coil_input-5": 789,
383
+ },
384
+ };
385
+
386
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
387
+ expect(result.channels["holding_register_input-100-float32"]).toBe(123);
388
+ expect(result.channels["register_input-200"]).toBe(456);
389
+ expect(result.channels["coil_input-5"]).toBe(789);
390
+ });
391
+
392
+ it("should handle odd schema types with arrays", () => {
393
+ const dataZ = caseconv.preserveCase(z.record(z.string(), z.unknown()));
394
+ const newZ = z.object({
395
+ data: dataZ,
396
+ });
397
+ const schema = z.object({
398
+ values: newZ.array(),
399
+ });
400
+ type Schema = z.infer<typeof schema>;
401
+ const v: Schema = { values: [{ data: { One: 1 } }] };
402
+ const result = caseconv.snakeToCamel(v, { schema });
403
+ expect(result.values[0].data.One).toBe(1);
404
+ });
405
+
406
+ it("should handle array.nullishToEmpty with preserveCase on element field", async () => {
407
+ const { nullishToEmpty } = await import("@/array/nullable");
408
+ const elementZ = z.object({
409
+ name: z.string(),
410
+ data: caseconv.preserveCase(z.record(z.string(), z.unknown())),
411
+ });
412
+ const schema = z.object({
413
+ items: nullishToEmpty(elementZ),
414
+ });
415
+ const input = {
416
+ items: [
417
+ {
418
+ name: "test",
419
+ data: {
420
+ camelCaseKey: "value1",
421
+ PascalCaseKey: "value2",
422
+ snake_case_key: "value3",
423
+ },
424
+ },
425
+ ],
426
+ };
427
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
428
+ expect(result.items[0].data.camelCaseKey).toBe("value1");
429
+ expect(result.items[0].data.PascalCaseKey).toBe("value2");
430
+ expect(result.items[0].data.snake_case_key).toBe("value3");
431
+ });
432
+ });
433
+
434
+ describe("complex schema compositions (regression tests)", () => {
435
+ it("should case-convert record keys through record.nullishToEmpty", () => {
436
+ const schema = z.object({
437
+ channels: record.nullishToEmpty(),
438
+ });
439
+
440
+ const input = {
441
+ channels: {
442
+ "ns=2;s=Temperature": 123,
443
+ holding_register_input: 456,
444
+ },
445
+ };
446
+
447
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
448
+ expect(result.channels["ns=2;s=Temperature"]).toBe(123);
449
+ expect(result.channels.holdingRegisterInput).toBe(456);
450
+ });
451
+
452
+ it("should preserve case through optional wrapper", () => {
453
+ const schema = z.object({
454
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())).optional(),
455
+ });
456
+
457
+ const input = {
458
+ channels: {
459
+ "ns=2;s=Test": 123,
460
+ snake_case_key: 456,
461
+ },
462
+ };
463
+
464
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
465
+ expect(result.channels["ns=2;s=Test"]).toBe(123);
466
+ expect(result.channels.snake_case_key).toBe(456);
467
+ expect(result.channels.snakeCaseKey).toBeUndefined();
468
+ });
469
+
470
+ it("should preserve case through nullable wrapper", () => {
471
+ const schema = z.object({
472
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())).nullable(),
473
+ });
474
+
475
+ const input = {
476
+ channels: {
477
+ my_channel_key: 789,
478
+ },
479
+ };
480
+
481
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
482
+ expect(result.channels.my_channel_key).toBe(789);
483
+ expect(result.channels.myChannelKey).toBeUndefined();
484
+ });
485
+
486
+ it("should preserve case through default wrapper", () => {
487
+ const schema = z.object({
488
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())).default({}),
489
+ });
490
+
491
+ const input = {
492
+ channels: {
493
+ default_test_key: 100,
494
+ },
495
+ };
496
+
497
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
498
+ expect(result.channels.default_test_key).toBe(100);
499
+ expect(result.channels.defaultTestKey).toBeUndefined();
500
+ });
501
+
502
+ it("should preserve case through catch wrapper", () => {
503
+ const schema = z.object({
504
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())).catch({}),
505
+ });
506
+
507
+ const input = {
508
+ channels: {
509
+ catch_test_key: 200,
510
+ },
511
+ };
512
+
513
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
514
+ expect(result.channels.catch_test_key).toBe(200);
515
+ expect(result.channels.catchTestKey).toBeUndefined();
516
+ });
517
+
518
+ it("should preserve case through transform/pipe wrapper", () => {
519
+ const schema = z.object({
520
+ channels: caseconv
521
+ .preserveCase(z.record(z.string(), z.number()))
522
+ .transform((v) => v),
523
+ });
524
+
525
+ const input = {
526
+ channels: {
527
+ transform_test_key: 300,
528
+ },
529
+ };
530
+
531
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
532
+ expect(result.channels.transform_test_key).toBe(300);
533
+ expect(result.channels.transformTestKey).toBeUndefined();
534
+ });
535
+
536
+ it("should preserve case through multiple nested wrappers", () => {
537
+ const schema = z.object({
538
+ channels: caseconv
539
+ .preserveCase(z.record(z.string(), z.number()))
540
+ .optional()
541
+ .nullable()
542
+ .default(null),
543
+ });
544
+
545
+ const input = {
546
+ channels: {
547
+ deeply_wrapped_key: 400,
548
+ },
549
+ };
550
+
551
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
552
+ expect(result.channels.deeply_wrapped_key).toBe(400);
553
+ expect(result.channels.deeplyWrappedKey).toBeUndefined();
554
+ });
555
+
556
+ it("should case-convert record keys with record.nullishToEmpty in nested object", () => {
557
+ const schema = z.object({
558
+ read: z.object({
559
+ index: z.number(),
560
+ channels: record.nullishToEmpty(),
561
+ }),
562
+ write: z.object({
563
+ channels: record.nullishToEmpty(),
564
+ }),
565
+ });
566
+
567
+ const input = {
568
+ read: {
569
+ index: 0,
570
+ channels: {
571
+ "ns=2;s=ReadChannel": 123,
572
+ holding_register_input: 456,
573
+ },
574
+ },
575
+ write: {
576
+ channels: {
577
+ coil_output: 789,
578
+ },
579
+ },
580
+ };
581
+
582
+ const result = caseconv.snakeToCamel(input, { schema }) as R;
583
+ expect(result.read.index).toBe(0);
584
+ expect(result.read.channels["ns=2;s=ReadChannel"]).toBe(123);
585
+ expect(result.read.channels.holdingRegisterInput).toBe(456);
586
+ expect(result.write.channels.coilOutput).toBe(789);
587
+ });
588
+
589
+ it("should handle null values with record.nullishToEmpty", () => {
590
+ const schema = z.object({
591
+ channels: record.nullishToEmpty(),
592
+ });
593
+
594
+ const inputNull = { channels: null };
595
+ const inputUndefined = { channels: undefined };
596
+
597
+ const resultNull = caseconv.snakeToCamel(inputNull, { schema });
598
+ const resultUndefined = caseconv.snakeToCamel(inputUndefined, {
599
+ schema,
600
+ });
601
+
602
+ // null/undefined should pass through (transform happens at validation time)
603
+ expect(resultNull.channels).toBeNull();
604
+ expect(resultUndefined.channels).toBeUndefined();
605
+ });
606
+ });
607
+
608
+ describe("schema lookup with camelToSnake (regression)", () => {
609
+ it("should find schema for preserveCase field when input has camelCase keys", async () => {
610
+ const { nullishToEmpty } = await import("@/array/nullable");
611
+ const elementZ = z.object({
612
+ name: z.string(),
613
+ data: caseconv.preserveCase(z.record(z.string(), z.unknown())),
614
+ });
615
+ const schema = z.object({
616
+ items: nullishToEmpty(elementZ),
617
+ });
618
+ const input = {
619
+ items: [
620
+ {
621
+ name: "test",
622
+ data: {
623
+ camelCaseKey: "value1",
624
+ PascalCaseKey: "value2",
625
+ snake_case_key: "value3",
626
+ },
627
+ },
628
+ ],
629
+ };
630
+ const result = caseconv.camelToSnake(input, { schema }) as R;
631
+ expect(result.items[0].data.camelCaseKey).toBe("value1");
632
+ expect(result.items[0].data.PascalCaseKey).toBe("value2");
633
+ expect(result.items[0].data.snake_case_key).toBe("value3");
634
+ expect(result.items[0].data.camel_case_key).toBeUndefined();
635
+ });
636
+
637
+ it("should preserve case through create/encode cycle with nullishToEmpty array", async () => {
638
+ const linePlotZ = z.object({
639
+ key: z.string().optional(),
640
+ name: z.string(),
641
+ data: caseconv.preserveCase(z.record(z.string(), z.unknown())),
642
+ });
643
+ const createReqZ = z.object({
644
+ workspace: z.string(),
645
+ linePlots: linePlotZ.array(),
646
+ });
647
+ const input = {
648
+ workspace: "ws-1",
649
+ linePlots: [
650
+ {
651
+ name: "Test",
652
+ data: {
653
+ myCustomKey: 123,
654
+ AnotherKey: { nested_value: 456 },
655
+ },
656
+ },
657
+ ],
658
+ };
659
+ const encoded = caseconv.camelToSnake(input, { schema: createReqZ }) as R;
660
+ expect(encoded.line_plots[0].data.myCustomKey).toBe(123);
661
+ expect(encoded.line_plots[0].data.AnotherKey.nested_value).toBe(456);
662
+ expect(encoded.line_plots[0].data.my_custom_key).toBeUndefined();
663
+ });
664
+
665
+ it("should preserve case through retrieve/decode cycle with nullishToEmpty array", async () => {
666
+ const { nullishToEmpty } = await import("@/array/nullable");
667
+ const linePlotZ = z.object({
668
+ key: z.string(),
669
+ name: z.string(),
670
+ data: caseconv.preserveCase(z.record(z.string(), z.unknown())),
671
+ });
672
+ const retrieveResZ = z.object({
673
+ line_plots: nullishToEmpty(linePlotZ),
674
+ });
675
+ const response = {
676
+ line_plots: [
677
+ {
678
+ key: "lp-1",
679
+ name: "Test",
680
+ data: {
681
+ myCustomKey: 123,
682
+ AnotherKey: { nested_value: 456 },
683
+ },
684
+ },
685
+ ],
686
+ };
687
+ const decoded = caseconv.snakeToCamel(response, {
688
+ schema: retrieveResZ,
689
+ }) as R;
690
+ expect(decoded.linePlots[0].data.myCustomKey).toBe(123);
691
+ expect(decoded.linePlots[0].data.AnotherKey.nested_value).toBe(456);
692
+ expect(decoded.linePlots[0].data.my_custom_key).toBeUndefined();
693
+ });
694
+ });
695
+ });
162
696
  });