@synnaxlabs/x 0.23.0 → 0.24.1

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 (108) hide show
  1. package/.turbo/turbo-build.log +40 -40
  2. package/dist/{base-D3FrPu8C.cjs → base-B48bPNx5.cjs} +1 -1
  3. package/dist/{base-Cz4DfmJf.js → base-DnZzEvvz.js} +11 -10
  4. package/dist/binary.cjs +3 -1
  5. package/dist/binary.js +83 -20
  6. package/dist/{bounds-1ouRnRJS.cjs → bounds-Bh_QBZZK.cjs} +1 -1
  7. package/dist/{bounds-BYz3mV9U.js → bounds-Dmn-hd_-.js} +1 -1
  8. package/dist/bounds.cjs +1 -1
  9. package/dist/bounds.js +1 -1
  10. package/dist/box-C92jYSNY.js +205 -0
  11. package/dist/box-m3dlc1kR.cjs +1 -0
  12. package/dist/box.cjs +1 -1
  13. package/dist/box.js +1 -1
  14. package/dist/caseconv.cjs +1 -1
  15. package/dist/caseconv.js +1 -1
  16. package/dist/compare.cjs +1 -1
  17. package/dist/compare.js +1 -1
  18. package/dist/deep.cjs +1 -1
  19. package/dist/deep.js +26 -24
  20. package/dist/{dimensions-9gLSFjFx.js → dimensions-BAuHfd-b.js} +1 -1
  21. package/dist/{dimensions-Fe8DhhXU.cjs → dimensions-zMcb9pMk.cjs} +1 -1
  22. package/dist/dimensions.cjs +1 -1
  23. package/dist/dimensions.js +1 -1
  24. package/dist/direction-CScbfCdT.js +16 -0
  25. package/dist/direction-DZbN47uL.cjs +1 -0
  26. package/dist/direction.cjs +1 -1
  27. package/dist/direction.js +1 -1
  28. package/dist/index-BhEWuRmo.cjs +1 -0
  29. package/dist/index-BpCPHf78.cjs +1 -0
  30. package/dist/{index-POWYBOZm.js → index-BwkQTA-j.js} +113 -109
  31. package/dist/index-IGDDLWef.js +96 -0
  32. package/dist/index.cjs +3 -1
  33. package/dist/index.js +167 -122
  34. package/dist/{location-C4YaXxjI.js → location-CfP9TAnW.js} +5 -5
  35. package/dist/{location-CZOgJmu5.cjs → location-c3g6WKs-.cjs} +1 -1
  36. package/dist/location.cjs +1 -1
  37. package/dist/location.js +1 -1
  38. package/dist/path-BO4pyGZf.cjs +1 -0
  39. package/dist/path-nTHmt_4i.js +65 -0
  40. package/dist/{position-BjhXUSEO.cjs → position-BBnkwGX1.cjs} +1 -1
  41. package/dist/{position-PSYLZzqw.js → position-B_aphAR0.js} +4 -4
  42. package/dist/position.cjs +1 -1
  43. package/dist/position.js +1 -1
  44. package/dist/{scale-DUBO0Q4s.cjs → scale-C8axypQ2.cjs} +1 -1
  45. package/dist/{scale-DCbi48di.js → scale-CgtgqlYU.js} +4 -4
  46. package/dist/scale.cjs +1 -1
  47. package/dist/scale.js +1 -1
  48. package/dist/{series-CSutMOtt.cjs → series-Cgjkfl5S.cjs} +1 -1
  49. package/dist/{series-DCv8f29w.js → series-w7kOJP17.js} +9 -9
  50. package/dist/{spatial-R5OeJgR6.cjs → spatial-BOhaO8xN.cjs} +1 -1
  51. package/dist/{spatial-6OGCmDve.js → spatial-DozyssiN.js} +1 -1
  52. package/dist/spatial.cjs +1 -1
  53. package/dist/spatial.js +9 -9
  54. package/dist/src/binary/encoder.d.ts +19 -0
  55. package/dist/src/binary/encoder.d.ts.map +1 -1
  56. package/dist/src/caseconv/caseconv.d.ts +2 -0
  57. package/dist/src/caseconv/caseconv.d.ts.map +1 -1
  58. package/dist/src/compare/compare.d.ts +1 -0
  59. package/dist/src/compare/compare.d.ts.map +1 -1
  60. package/dist/src/compare/compare.spec.d.ts +2 -0
  61. package/dist/src/compare/compare.spec.d.ts.map +1 -0
  62. package/dist/src/deep/path.d.ts +13 -4
  63. package/dist/src/deep/path.d.ts.map +1 -1
  64. package/dist/src/migrate/migrate.d.ts +45 -10
  65. package/dist/src/migrate/migrate.d.ts.map +1 -1
  66. package/dist/src/spatial/box/box.d.ts +4 -2
  67. package/dist/src/spatial/box/box.d.ts.map +1 -1
  68. package/dist/src/spatial/direction/direction.d.ts +1 -1
  69. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  70. package/dist/src/spatial/xy/xy.d.ts +1 -1
  71. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  72. package/dist/src/zodutil/zodutil.d.ts.map +1 -1
  73. package/dist/telem.cjs +1 -1
  74. package/dist/telem.js +1 -1
  75. package/dist/{xy-H8aUVW_X.js → xy-D1ZbIqpT.js} +12 -12
  76. package/dist/xy-cP-FXJtR.cjs +1 -0
  77. package/dist/xy.cjs +1 -1
  78. package/dist/xy.js +1 -1
  79. package/dist/zodutil.cjs +1 -1
  80. package/dist/zodutil.js +2 -3
  81. package/package.json +3 -3
  82. package/src/binary/encoder.spec.ts +121 -3
  83. package/src/binary/encoder.ts +91 -1
  84. package/src/caseconv/caseconv.ts +10 -1
  85. package/src/compare/compare.spec.ts +49 -0
  86. package/src/compare/compare.ts +32 -2
  87. package/src/deep/path.spec.ts +3 -2
  88. package/src/deep/path.ts +27 -21
  89. package/src/migrate/migrate.spec.ts +62 -54
  90. package/src/migrate/migrate.ts +131 -19
  91. package/src/spatial/box/box.spec.ts +70 -22
  92. package/src/spatial/box/box.ts +15 -11
  93. package/src/spatial/direction/direction.ts +2 -2
  94. package/src/spatial/xy/xy.ts +3 -2
  95. package/src/telem/telem.spec.ts +35 -10
  96. package/src/zodutil/zodutil.spec.ts +2 -2
  97. package/src/zodutil/zodutil.ts +2 -3
  98. package/tsconfig.tsbuildinfo +1 -1
  99. package/dist/box-CHDDKvN3.cjs +0 -1
  100. package/dist/box-Q7p9JFnX.js +0 -204
  101. package/dist/direction-dXjlmg4_.cjs +0 -1
  102. package/dist/direction-wsPTe9a1.js +0 -15
  103. package/dist/index-Bcm6lEr8.js +0 -73
  104. package/dist/index-CnZb0cyW.cjs +0 -1
  105. package/dist/index-xjptNL6Z.cjs +0 -1
  106. package/dist/path-BfHj5x9k.js +0 -62
  107. package/dist/path-By98WcWm.cjs +0 -1
  108. package/dist/xy-B4SvvNaG.cjs +0 -1
@@ -12,7 +12,7 @@ import { z } from "zod";
12
12
 
13
13
  import { binary } from "@/binary";
14
14
 
15
- const SampleSchema = z.object({
15
+ const sampleSchema = z.object({
16
16
  channelKey: z.string(),
17
17
  timeStamp: z.number(),
18
18
  value: z.unknown(),
@@ -27,7 +27,7 @@ binary.ENCODERS.forEach((e) => {
27
27
  value: new Array([1, 2, 3]),
28
28
  };
29
29
  const encoded = e.encode(sample);
30
- expect(e.decode(encoded, SampleSchema)).toEqual(sample);
30
+ expect(e.decode(encoded, sampleSchema)).toEqual(sample);
31
31
  });
32
32
  });
33
33
  });
@@ -50,7 +50,125 @@ describe("JSON", () => {
50
50
  value: new Array([1, 2, 3]),
51
51
  };
52
52
  const encoded = JSON.stringify(sample);
53
- const decoded = binary.JSON_ECD.decodeString(encoded, SampleSchema);
53
+ const decoded = binary.JSON_ECD.decodeString(encoded, sampleSchema);
54
54
  expect(decoded.channelKey).toEqual("test");
55
55
  });
56
+
57
+ describe("CSVEncoderDecoder", () => {
58
+ it("should correctly decode CSV data with valid input", () => {
59
+ const sample = `
60
+ channelKey,timeStamp,value
61
+ test,123,5
62
+ test2,124,6
63
+ `;
64
+ const decoded = binary.CSV_ECD.decodeString(sample);
65
+ expect(decoded).toEqual({
66
+ channelKey: ["test", "test2"],
67
+ timeStamp: [123, 124],
68
+ value: [5, 6],
69
+ });
70
+ });
71
+
72
+ it("should handle empty CSV data", () => {
73
+ const sample = `
74
+ `;
75
+ const decoded = binary.CSV_ECD.decodeString(sample);
76
+ expect(decoded).toEqual({});
77
+ });
78
+
79
+ it("should handle CSV with only headers", () => {
80
+ const sample = `
81
+ channelKey,timeStamp,value
82
+ `;
83
+ const decoded = binary.CSV_ECD.decodeString(sample);
84
+ expect(decoded).toEqual({
85
+ channelKey: [],
86
+ timeStamp: [],
87
+ value: [],
88
+ });
89
+ });
90
+
91
+ it("should handle CSV with missing values", () => {
92
+ const sample = `
93
+ channelKey,timeStamp,value
94
+ test,123,
95
+ test2,124,6
96
+ `;
97
+ const decoded = binary.CSV_ECD.decodeString(sample);
98
+ expect(decoded).toEqual({
99
+ channelKey: ["test", "test2"],
100
+ timeStamp: [123, 124],
101
+ value: [6],
102
+ });
103
+ });
104
+
105
+ it("should handle CSV with extra values", () => {
106
+ const sample = `
107
+ channelKey,timeStamp,value
108
+ test,123,5,extra
109
+ test2,124,6
110
+ `;
111
+ const decoded = binary.CSV_ECD.decodeString(sample);
112
+ expect(decoded).toEqual({
113
+ channelKey: ["test", "test2"],
114
+ timeStamp: [123, 124],
115
+ value: [5, 6],
116
+ });
117
+ });
118
+
119
+ it("should handle CSV with different types of values", () => {
120
+ const sample = `
121
+ key,number,string
122
+ test,123,"hello"
123
+ test2,456,"world"
124
+ `;
125
+ const decoded = binary.CSV_ECD.decodeString(sample);
126
+ expect(decoded).toEqual({
127
+ key: ["test", "test2"],
128
+ number: [123, 456],
129
+ string: ["hello", "world"],
130
+ });
131
+ });
132
+
133
+ it("should handle CSV with spaces around values", () => {
134
+ const sample = `
135
+ key, number , string
136
+ test , 123 , "hello"
137
+ test2 , 456 , "world"
138
+ `;
139
+ const decoded = binary.CSV_ECD.decodeString(sample);
140
+ expect(decoded).toEqual({
141
+ key: ["test", "test2"],
142
+ number: [123, 456],
143
+ string: ["hello", "world"],
144
+ });
145
+ });
146
+
147
+ it("should handle CSV with empty rows", () => {
148
+ const sample = `
149
+ key,number,string
150
+ test,123,"hello"
151
+ ,
152
+ test2,456,"world"
153
+ `;
154
+ const decoded = binary.CSV_ECD.decodeString(sample);
155
+ expect(decoded).toEqual({
156
+ key: ["test", "test2"],
157
+ number: [123, 456],
158
+ string: ["hello", "world"],
159
+ });
160
+ });
161
+
162
+ it("should handle CSV with single column", () => {
163
+ const sample = `
164
+ key
165
+ test
166
+ test2
167
+ `;
168
+ const decoded = binary.CSV_ECD.decodeString(sample);
169
+ expect(decoded).toEqual({
170
+ key: ["test", "test2"],
171
+ });
172
+ });
173
+ });
56
174
  });
@@ -7,7 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { type z,type ZodSchema } from "zod";
10
+ import { type z, type ZodSchema } from "zod";
11
11
 
12
12
  import { caseconv } from "@/caseconv";
13
13
  import { isObject } from "@/identity";
@@ -79,6 +79,96 @@ export class JSONEncoderDecoder implements EncoderDecoder {
79
79
  static registerCustomType(): void {}
80
80
  }
81
81
 
82
+ /**
83
+ * CSVEncoderDecoder is a CSV implementation of EncoderDecoder.
84
+ */
85
+ export class CSVEncoderDecoder implements EncoderDecoder {
86
+ contentType = "text/csv";
87
+
88
+ encode(payload: unknown): ArrayBuffer {
89
+ const csvString = this.encodeString(payload);
90
+ return new TextEncoder().encode(csvString).buffer;
91
+ }
92
+
93
+ decode<P extends z.ZodTypeAny>(
94
+ data: Uint8Array | ArrayBuffer,
95
+ schema?: P,
96
+ ): z.output<P> {
97
+ const csvString = new TextDecoder().decode(data);
98
+ return this.decodeString(csvString, schema);
99
+ }
100
+
101
+ encodeString(payload: unknown): string {
102
+ if (!Array.isArray(payload) || payload.length === 0 || !isObject(payload[0])) {
103
+ throw new Error("Payload must be an array of objects");
104
+ }
105
+
106
+ const keys = Object.keys(payload[0]);
107
+ const csvRows = [keys.join(",")];
108
+
109
+ payload.forEach((item: any) => {
110
+ const values = keys.map((key) => JSON.stringify(item[key] ?? ""));
111
+ csvRows.push(values.join(","));
112
+ });
113
+
114
+ return csvRows.join("\n");
115
+ }
116
+
117
+ decodeString<P extends z.ZodTypeAny>(data: string, schema?: P): z.output<P> {
118
+ const [headerLine, ...lines] = data
119
+ .trim()
120
+ .split("\n")
121
+ .map((line) => line.trim());
122
+ if (headerLine.length === 0)
123
+ return schema != null ? schema.parse({}) : ({} as z.output<P>);
124
+ const headers = headerLine.split(",").map((header) => header.trim());
125
+ const result: { [key: string]: any[] } = {};
126
+
127
+ headers.forEach((header) => {
128
+ result[header] = [];
129
+ });
130
+
131
+ lines.forEach((line) => {
132
+ const values = line.split(",").map((value) => value.trim());
133
+ headers.forEach((header, index) => {
134
+ const v = this.parseValue(values[index]);
135
+ if (v == null) return;
136
+ result[header].push(v);
137
+ });
138
+ });
139
+
140
+ return schema != null ? schema.parse(result) : (result as z.output<P>);
141
+ }
142
+
143
+ private parseValue(value?: string): any {
144
+ if (value == null || value.length === 0) return null;
145
+ const num = Number(value);
146
+ if (!isNaN(num)) return num;
147
+ if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
148
+ return value;
149
+ }
150
+
151
+ static registerCustomType(): void {}
152
+ }
153
+
154
+ export class TextEncoderDecoder implements EncoderDecoder {
155
+ contentType = "text/plain";
156
+
157
+ encode(payload: unknown): ArrayBuffer {
158
+ return new TextEncoder().encode(payload as string).buffer;
159
+ }
160
+
161
+ decode<P extends z.ZodTypeAny>(
162
+ data: Uint8Array | ArrayBuffer,
163
+ schema?: P,
164
+ ): z.output<P> {
165
+ const text = new TextDecoder().decode(data);
166
+ return schema != null ? schema.parse(text) : (text as z.output<P>);
167
+ }
168
+ }
169
+
82
170
  export const JSON_ECD = new JSONEncoderDecoder();
171
+ export const CSV_ECD = new CSVEncoderDecoder();
172
+ export const TEXT_ECD = new TextEncoderDecoder();
83
173
 
84
174
  export const ENCODERS: EncoderDecoder[] = [JSON_ECD];
@@ -7,7 +7,12 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { camelKeys as _camelKeys, snakeKeys as _snakeKeys } from "js-convert-case";
10
+ import {
11
+ camelKeys as _camelKeys,
12
+ snakeKeys as _snakeKeys,
13
+ toCamelCase as _toCamelCase,
14
+ toSnakeCase as _toSnakeCase,
15
+ } from "js-convert-case";
11
16
 
12
17
  const options = {
13
18
  recursive: true,
@@ -20,3 +25,7 @@ export const toSnake = <T>(entity: T): T => _snakeKeys(entity, options) as T;
20
25
  export const toCamel = <T>(entity: T): T => _camelKeys(entity, options) as T;
21
26
 
22
27
  export const capitalize = (str: string): string => str[0].toUpperCase() + str.slice(1);
28
+
29
+ export const stringToSnake = (str: string): string => _toSnakeCase(str);
30
+
31
+ export const stringToCamel = (str: string): string => _toCamelCase(str);
@@ -0,0 +1,49 @@
1
+ // Copyright 2024 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import { compare } from "@/compare";
13
+
14
+ describe("compare", () => {
15
+ describe("numericStrings", () => {
16
+ interface Spec {
17
+ name: string;
18
+ input: string[];
19
+ expected: string[];
20
+ }
21
+ const SPECS: Spec[] = [
22
+ {
23
+ name: "pure numbers",
24
+ input: ["5", "4", "3", "2", "1"],
25
+ expected: ["1", "2", "3", "4", "5"],
26
+ },
27
+ {
28
+ name: "suffixed numbers with equal prefixes",
29
+ input: ["a5", "a4", "a3", "a2", "a1"],
30
+ expected: ["a1", "a2", "a3", "a4", "a5"],
31
+ },
32
+ {
33
+ name: "suffixed numbers with different prefixes",
34
+ input: ["a1", "b1", "a2", "b2", "a3", "b3"],
35
+ expected: ["a1", "a2", "a3", "b1", "b2", "b3"],
36
+ },
37
+ {
38
+ name: "mixed separators",
39
+ input: ["a2", "a.1", "a-3", "a_4"],
40
+ expected: ["a.1", "a2", "a-3", "a_4"],
41
+ },
42
+ ];
43
+ SPECS.forEach((spec) => {
44
+ it(spec.name, () => {
45
+ expect(spec.input.sort(compare.stringsWithNumbers)).toEqual(spec.expected);
46
+ });
47
+ });
48
+ });
49
+ });
@@ -7,7 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { isStringer,type Primitive } from "@/primitive";
10
+ import { isStringer, type Primitive } from "@/primitive";
11
11
  import { type spatial } from "@/spatial";
12
12
 
13
13
  export type CompareF<T> = (a: T, b: T) => number;
@@ -40,8 +40,11 @@ export const newF = <T>(v: T, reverse: boolean = false): CompareF<T> => {
40
40
  case "boolean":
41
41
  f = (a: T, b: T) => Number(a) - Number(b);
42
42
  break;
43
+ case "undefined":
44
+ f = () => 0;
45
+ break;
43
46
  default:
44
- console.warn("sortFunc: unknown type");
47
+ console.warn(`sortFunc: unknown type ${t}`);
45
48
  return () => -1;
46
49
  }
47
50
  return reverse ? reverseF(f) : f;
@@ -117,3 +120,30 @@ export const isLessThan = (n: number): boolean => n < EQUAL;
117
120
  export const isGreaterThan = (n: number): boolean => n > EQUAL;
118
121
 
119
122
  export const isGreaterThanEqual = (n: number): boolean => n >= EQUAL;
123
+
124
+ export const stringsWithNumbers = (a: string, b: string): number => {
125
+ const alphaNumericRegex = /([a-zA-Z]+)|(\d+)/g;
126
+
127
+ // Remove separators and split into parts
128
+ const aParts = a.replace(/[\s_.\-]+/g, "").match(alphaNumericRegex);
129
+ const bParts = b.replace(/[\s_.\-]+/g, "").match(alphaNumericRegex);
130
+
131
+ if (!aParts || !bParts) return 0;
132
+
133
+ for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
134
+ const aPart = aParts[i];
135
+ const bPart = bParts[i];
136
+
137
+ if (isNaN(Number(aPart)) && isNaN(Number(bPart))) {
138
+ const localeComparison = aPart.localeCompare(bPart);
139
+ if (localeComparison !== 0) return localeComparison;
140
+ } else if (!isNaN(Number(aPart)) && !isNaN(Number(bPart))) {
141
+ const numComparison = Number(aPart) - Number(bPart);
142
+ if (numComparison !== 0) return numComparison;
143
+ } else {
144
+ return isNaN(Number(aPart)) ? -1 : 1;
145
+ }
146
+ }
147
+
148
+ return aParts.length - bParts.length;
149
+ };
@@ -7,7 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { describe, expect,it } from "vitest";
10
+ import { describe, expect, it } from "vitest";
11
11
 
12
12
  import { deep } from "@/deep";
13
13
  import { UnknownRecord } from "@/record";
@@ -63,7 +63,8 @@ describe("path", () => {
63
63
  };
64
64
  it("should use the custom getter function", () => {
65
65
  expect(
66
- deep.get(v, "a.value().c", false, {
66
+ deep.get(v, "a.value().c", {
67
+ optional: false,
67
68
  getter: (obj, key) => {
68
69
  if (key === "value()")
69
70
  return (obj as { value: () => { c: number } }).value();
package/src/deep/path.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
+ import { caseconv } from "@/caseconv";
10
11
  import { type Join } from "@/join";
11
12
  import { type UnknownRecord } from "@/record";
12
13
 
@@ -46,29 +47,28 @@ export type Key<T, D extends number = 5> = [D] extends [never]
46
47
  }[keyof T]
47
48
  : "";
48
49
 
49
- export interface GetOptions {
50
- getter?: (obj: unknown, key: string) => unknown;
50
+ export interface GetOptions<O extends boolean | undefined = boolean | undefined> {
51
+ optional: O;
52
+ getter?: (obj: UnknownRecord, key: string) => unknown;
51
53
  }
52
54
 
53
- export type Get = (<V = unknown, T = UnknownRecord>(
54
- obj: T,
55
- path: string,
56
- allowNull: true,
57
- options?: GetOptions,
58
- ) => V | null) &
59
- (<V = unknown, T = UnknownRecord>(
55
+ export interface Get {
56
+ <V = unknown, T = UnknownRecord>(
60
57
  obj: T,
61
58
  path: string,
62
- allowNull?: boolean,
63
- options?: GetOptions,
64
- ) => V);
59
+ options?: GetOptions<false>,
60
+ ): V;
61
+ <V = unknown, T = UnknownRecord>(
62
+ obj: T,
63
+ path: string,
64
+ options?: GetOptions<boolean | undefined>,
65
+ ): V | null;
66
+ }
65
67
 
66
- export type TypedGet<V = unknown, T = UnknownRecord> = ((
67
- obj: T,
68
- path: string,
69
- allowNull: true,
70
- ) => V | null) &
71
- ((obj: T, path: string, allowNull?: boolean) => V);
68
+ export interface TypedGet<V = unknown, T = UnknownRecord> {
69
+ (obj: T, path: string, options?: GetOptions<false>): V;
70
+ (obj: T, path: string, options?: GetOptions<boolean | undefined>): V | null;
71
+ }
72
72
 
73
73
  export const transformPath = (
74
74
  path: string,
@@ -90,19 +90,25 @@ export const transformPath = (
90
90
  return result.join(".");
91
91
  };
92
92
 
93
+ export const pathToSnake = (path: string): string =>
94
+ transformPath(path, caseconv.stringToSnake);
95
+
96
+ export const pathToCamel = (path: string): string =>
97
+ transformPath(path, caseconv.stringToCamel);
98
+
93
99
  export const get = (<V = unknown, T = UnknownRecord>(
94
100
  obj: T,
95
101
  path: string,
96
- allowNull: boolean = false,
97
- { getter = (obj, key) => (obj as UnknownRecord)[key] }: GetOptions = {},
102
+ opts: GetOptions = { optional: false },
98
103
  ): V | null => {
104
+ const { optional, getter = (obj, key) => (obj as UnknownRecord)[key] } = opts;
99
105
  const parts = path.split(".");
100
106
  if (parts.length === 1 && parts[0] === "") return obj as unknown as V;
101
107
  let result: UnknownRecord = obj as UnknownRecord;
102
108
  for (const part of parts) {
103
109
  const v = getter(result, part);
104
110
  if (v == null) {
105
- if (allowNull) return null;
111
+ if (optional) return null;
106
112
  throw new Error(`Path ${path} does not exist. ${part} is null`);
107
113
  }
108
114
  result = v as UnknownRecord;
@@ -10,80 +10,88 @@
10
10
  import { describe, expect, it } from "vitest";
11
11
  import { z } from "zod";
12
12
 
13
- import {
14
- migratable,
15
- type Migration,
16
- type Migrations,
17
- migrator,
18
- } from "@/migrate/migrate";
13
+ import { migrate } from "@/migrate";
19
14
 
20
- const entityV0_0_0 = migratable.extend({
15
+ const entityV0 = z.object({
16
+ version: z.literal("0.0.0"),
21
17
  name: z.string(),
22
18
  });
23
19
 
24
- type EntityV0_0_0 = z.infer<typeof entityV0_0_0>;
20
+ type EntityV0 = z.infer<typeof entityV0>;
25
21
 
26
- const entityV0_0_1 = migratable.extend({
22
+ const entityV1 = z.object({
23
+ version: z.literal("1.0.0"),
27
24
  title: z.string(),
28
25
  });
29
26
 
30
- type EntityV0_0_1 = z.infer<typeof entityV0_0_1>;
27
+ const migrateV1 = migrate.createMigration<EntityV0, EntityV1>({
28
+ name: "entity",
29
+ inputSchema: entityV0,
30
+ outputSchema: entityV1,
31
+ migrate: (entity) => {
32
+ const { name, ...rest } = entity;
33
+ return { ...rest, version: "1.0.0", title: entity.name };
34
+ },
35
+ });
31
36
 
32
- const entityV0_0_2 = migratable.extend({
37
+ type EntityV1 = z.infer<typeof entityV1>;
38
+
39
+ const entityV2 = z.object({
40
+ version: z.literal("2.0.0"),
33
41
  title: z.string(),
34
42
  description: z.string(),
35
43
  });
36
44
 
37
- type EntityV0_0_2 = z.infer<typeof entityV0_0_2>;
45
+ type EntityV2 = z.infer<typeof entityV2>;
38
46
 
39
- const migrations: Migrations = {
40
- "0.0.0": ((entity: EntityV0_0_0): EntityV0_0_1 => {
47
+ const migrateV2 = migrate.createMigration<EntityV1, EntityV2>({
48
+ name: "entity",
49
+ inputSchema: entityV1,
50
+ outputSchema: entityV2,
51
+ migrate: (entity) => ({ ...entity, version: "2.0.0", description: "" }),
52
+ });
41
53
 
42
- const { name, ...rest } = entity;
43
- return {
44
- ...rest,
45
- version: "0.0.1",
46
- title: entity.name,
47
- };
48
- }) as Migration<EntityV0_0_0, EntityV0_0_1>,
49
- "0.0.1": (entity: EntityV0_0_1): EntityV0_0_2 => {
50
- return {
51
- ...entity,
52
- version: "0.0.2",
53
- description: "",
54
- };
55
- },
54
+ const migrations: migrate.Migrations = {
55
+ "0.0.0": migrateV1,
56
+ "1.0.0": migrateV2,
56
57
  };
57
58
 
59
+ describe("compareSemVer", () => {
60
+ it("should return true when the major version is higher", () => {
61
+ expect(migrate.compareSemVer("1.0.0", "0.0.0")).toBeGreaterThan(0);
62
+ expect(migrate.semVerNewer("3.0.0", "0.3.0")).toBeTruthy();
63
+ });
64
+ });
65
+
58
66
  describe("migrator", () => {
59
- it("should migrate an entity from v0.0.0 to v0.0.2", () => {
60
- const entity: EntityV0_0_0 = {
61
- version: "0.0.0",
62
- name: "foo",
63
- };
64
- const migrated = migrator(migrations)(entity);
65
- expect(migrated).toEqual({
66
- version: "0.0.2",
67
- title: "foo",
68
- description: "",
69
- });
67
+ it("should migrate an entity from v0 to v2", () => {
68
+ const entity: EntityV0 = { version: "0.0.0", name: "foo" };
69
+ const DEFAULT: EntityV2 = { version: "2.0.0", title: "", description: "" };
70
+ const migrated = migrate.migrator({
71
+ name: "entity",
72
+ migrations,
73
+ def: DEFAULT,
74
+ })(entity);
75
+ expect(migrated).toEqual({ version: "2.0.0", title: "foo", description: "" });
70
76
  });
71
- it("should not migrate an entity from v0.0.2", () => {
72
- const entity: EntityV0_0_2 = {
73
- version: "0.0.2",
74
- title: "foo",
75
- description: "bar",
76
- };
77
- const migrated = migrator(migrations)(entity);
78
- expect(migrated).toEqual(entity);
77
+ it("should migrate an entity from v1 to v2", () => {
78
+ const entity: EntityV1 = { version: "1.0.0", title: "foo" };
79
+ const DEFAULT: EntityV2 = { version: "2.0.0", title: "", description: "" };
80
+ const migrated = migrate.migrator({
81
+ name: "entity",
82
+ migrations,
83
+ def: DEFAULT,
84
+ })(entity);
85
+ expect(migrated).toEqual({ version: "2.0.0", title: "foo", description: "" });
79
86
  });
80
- it("should not migrate an entity from v0.0.3", () => {
81
- const entity = {
82
- version: "0.0.3",
83
- title: "foo",
84
- description: "bar",
85
- };
86
- const migrated = migrator(migrations)(entity);
87
+ it("should not migrate an entity from v2 to v2", () => {
88
+ const entity: EntityV2 = { version: "2.0.0", title: "foo", description: "bar" };
89
+ const DEFAULT: EntityV2 = { version: "2.0.0", title: "", description: "" };
90
+ const migrated = migrate.migrator({
91
+ name: "entity",
92
+ migrations,
93
+ def: DEFAULT,
94
+ })(entity);
87
95
  expect(migrated).toEqual(entity);
88
96
  });
89
97
  });