@synnaxlabs/x 0.54.1 → 0.54.2

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 (64) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/dist/src/binary/codec.d.ts.map +1 -1
  3. package/dist/src/color/color.d.ts +1 -1
  4. package/dist/src/color/palette.d.ts +2 -2
  5. package/dist/src/deep/atKeys.d.ts +27 -0
  6. package/dist/src/deep/atKeys.d.ts.map +1 -0
  7. package/dist/src/deep/atKeys.spec.d.ts +2 -0
  8. package/dist/src/deep/atKeys.spec.d.ts.map +1 -0
  9. package/dist/src/deep/external.d.ts +1 -0
  10. package/dist/src/deep/external.d.ts.map +1 -1
  11. package/dist/src/fmt/external.d.ts +3 -0
  12. package/dist/src/fmt/external.d.ts.map +1 -0
  13. package/dist/src/fmt/index.d.ts +2 -0
  14. package/dist/src/fmt/index.d.ts.map +1 -0
  15. package/dist/src/fmt/path.d.ts +13 -0
  16. package/dist/src/fmt/path.d.ts.map +1 -0
  17. package/dist/src/fmt/path.spec.d.ts +2 -0
  18. package/dist/src/fmt/path.spec.d.ts.map +1 -0
  19. package/dist/src/fmt/value.d.ts +23 -0
  20. package/dist/src/fmt/value.d.ts.map +1 -0
  21. package/dist/src/fmt/value.spec.d.ts +2 -0
  22. package/dist/src/fmt/value.spec.d.ts.map +1 -0
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/label/types.gen.d.ts +2 -2
  26. package/dist/src/narrow/narrow.d.ts +9 -0
  27. package/dist/src/narrow/narrow.d.ts.map +1 -1
  28. package/dist/src/primitive/primitive.d.ts +10 -0
  29. package/dist/src/primitive/primitive.d.ts.map +1 -1
  30. package/dist/src/status/status.d.ts +14 -2
  31. package/dist/src/status/status.d.ts.map +1 -1
  32. package/dist/src/status/types.gen.d.ts +1 -1
  33. package/dist/src/telem/series.d.ts +4 -4
  34. package/dist/src/telem/telem.d.ts +10 -10
  35. package/dist/src/zod/external.d.ts +1 -0
  36. package/dist/src/zod/external.d.ts.map +1 -1
  37. package/dist/src/zod/parse.d.ts +47 -0
  38. package/dist/src/zod/parse.d.ts.map +1 -0
  39. package/dist/src/zod/parse.spec.d.ts +2 -0
  40. package/dist/src/zod/parse.spec.d.ts.map +1 -0
  41. package/dist/x.cjs +16 -7
  42. package/dist/x.js +2190 -1869
  43. package/package.json +3 -3
  44. package/src/binary/codec.ts +3 -2
  45. package/src/deep/atKeys.spec.ts +107 -0
  46. package/src/deep/atKeys.ts +49 -0
  47. package/src/deep/external.ts +1 -0
  48. package/src/fmt/external.ts +11 -0
  49. package/src/fmt/index.ts +10 -0
  50. package/src/fmt/path.spec.ts +46 -0
  51. package/src/fmt/path.ts +30 -0
  52. package/src/fmt/value.spec.ts +206 -0
  53. package/src/fmt/value.ts +83 -0
  54. package/src/index.ts +1 -0
  55. package/src/narrow/narrow.spec.ts +43 -0
  56. package/src/narrow/narrow.ts +15 -0
  57. package/src/primitive/primitive.spec.ts +51 -0
  58. package/src/primitive/primitive.ts +12 -0
  59. package/src/status/status.spec.ts +146 -0
  60. package/src/status/status.ts +65 -18
  61. package/src/zod/external.ts +1 -0
  62. package/src/zod/parse.spec.ts +702 -0
  63. package/src/zod/parse.ts +519 -0
  64. package/tsconfig.tsbuildinfo +1 -1
@@ -64,6 +64,57 @@ describe("primitive", () => {
64
64
  });
65
65
  });
66
66
 
67
+ describe("is", () => {
68
+ it("should return true for null and undefined", () => {
69
+ expect(primitive.is(null)).toBe(true);
70
+ expect(primitive.is(undefined)).toBe(true);
71
+ });
72
+
73
+ it("should return true for strings", () => {
74
+ expect(primitive.is("")).toBe(true);
75
+ expect(primitive.is("hello")).toBe(true);
76
+ });
77
+
78
+ it("should return true for numbers", () => {
79
+ expect(primitive.is(0)).toBe(true);
80
+ expect(primitive.is(3.14)).toBe(true);
81
+ expect(primitive.is(NaN)).toBe(true);
82
+ });
83
+
84
+ it("should return true for booleans", () => {
85
+ expect(primitive.is(true)).toBe(true);
86
+ expect(primitive.is(false)).toBe(true);
87
+ });
88
+
89
+ it("should return true for bigints", () => {
90
+ expect(primitive.is(42n)).toBe(true);
91
+ });
92
+
93
+ it("should return true for symbols", () => {
94
+ expect(primitive.is(Symbol("x"))).toBe(true);
95
+ });
96
+
97
+ it("should return false for plain objects", () => {
98
+ expect(primitive.is({})).toBe(false);
99
+ expect(primitive.is({ a: 1 })).toBe(false);
100
+ });
101
+
102
+ it("should return false for arrays", () => {
103
+ expect(primitive.is([])).toBe(false);
104
+ expect(primitive.is([1, 2, 3])).toBe(false);
105
+ });
106
+
107
+ it("should return false for class instances", () => {
108
+ expect(primitive.is(new Date())).toBe(false);
109
+ expect(primitive.is(new Map())).toBe(false);
110
+ expect(primitive.is(new Error())).toBe(false);
111
+ });
112
+
113
+ it("should return false for functions", () => {
114
+ expect(primitive.is(() => 1)).toBe(false);
115
+ });
116
+ });
117
+
67
118
  describe("ValueExtension", () => {
68
119
  class MyValueExtension extends primitive.ValueExtension<bigint> {
69
120
  valueOf(): bigint {
@@ -105,3 +105,15 @@ export const isZero = <V extends Value>(value: V): value is V & ZeroValue => {
105
105
  */
106
106
  export const isNonZero = <V extends Value>(value: V): value is V & NonZeroValue =>
107
107
  !isZero(value);
108
+
109
+ /**
110
+ * @returns true if the given value is a JavaScript primitive: `null`, `undefined`,
111
+ * or a value whose `typeof` is not `"object"` or `"function"`. Returns false for
112
+ * objects (including arrays), functions, and class instances.
113
+ *
114
+ * Complements {@link isZero} and {@link isNonZero}, which check for the zero value
115
+ * of a known primitive type. Use `is` when you have an `unknown` value and want to
116
+ * decide whether to render it inline vs. recurse into it.
117
+ */
118
+ export const is = (value: unknown): boolean =>
119
+ value == null || (typeof value !== "object" && typeof value !== "function");
@@ -14,6 +14,8 @@ import { id } from "@/id";
14
14
  import { status } from "@/status";
15
15
  import { TimeStamp } from "@/telem";
16
16
 
17
+ type CustomCrude = Partial<status.Crude<z.ZodRecord, "error">>;
18
+
17
19
  describe("status", () => {
18
20
  describe("create", () => {
19
21
  it("should create a status", () => {
@@ -129,6 +131,117 @@ describe("status", () => {
129
131
  const result = status.exceptionDetailsSchema.safeParse(s.details);
130
132
  expect(result.success).toBe(true);
131
133
  });
134
+
135
+ describe("custom errors (toStatus)", () => {
136
+ class CustomError extends Error implements status.Custom {
137
+ constructor(message: string) {
138
+ super(message);
139
+ this.name = "CustomError";
140
+ }
141
+
142
+ toStatus(): CustomCrude {
143
+ return {
144
+ message: "custom headline",
145
+ description: "custom description body",
146
+ details: { code: 42, hint: "try again" },
147
+ };
148
+ }
149
+ }
150
+
151
+ it("should use toStatus.message as the status message", () => {
152
+ const s = status.fromException(new CustomError("raw"));
153
+ expect(s.message).toBe("custom headline");
154
+ });
155
+
156
+ it("should use toStatus.description as the status description", () => {
157
+ const s = status.fromException(new CustomError("raw"));
158
+ expect(s.description).toBe("custom description body");
159
+ });
160
+
161
+ it("should merge toStatus.details into status.details", () => {
162
+ const s = status.fromException(new CustomError("raw"));
163
+ const details = s.details as Record<string, unknown>;
164
+ expect(details.code).toBe(42);
165
+ expect(details.hint).toBe("try again");
166
+ expect(details.stack).toBeDefined();
167
+ expect(details.error).toBeInstanceOf(Error);
168
+ });
169
+
170
+ it("should prefix toStatus.message when the caller provides a custom message", () => {
171
+ const s = status.fromException(new CustomError("raw"), "Saving failed");
172
+ expect(s.message).toBe("Saving failed: custom headline");
173
+ });
174
+
175
+ it("should fall through to the default path for errors without toStatus", () => {
176
+ const s = status.fromException(new Error("plain"));
177
+ expect(s.message).toBe("plain");
178
+ expect(s.description).toBeUndefined();
179
+ });
180
+
181
+ it("should ignore toStatus if it throws", () => {
182
+ class Bad extends Error {
183
+ toStatus() {
184
+ throw new Error("explode");
185
+ }
186
+ }
187
+ const s = status.fromException(new Bad("fallback"));
188
+ expect(s.message).toBe("fallback");
189
+ expect(s.description).toBeUndefined();
190
+ });
191
+
192
+ it("should ignore toStatus if it returns a non-object", () => {
193
+ class Bad extends Error {
194
+ toStatus() {
195
+ return "nope" as unknown as CustomCrude;
196
+ }
197
+ }
198
+ const s = status.fromException(new Bad("fallback"));
199
+ expect(s.message).toBe("fallback");
200
+ });
201
+
202
+ it("should ignore toStatus if it returns null", () => {
203
+ class Bad extends Error {
204
+ toStatus() {
205
+ return null as unknown as CustomCrude;
206
+ }
207
+ }
208
+ const s = status.fromException(new Bad("fallback"));
209
+ expect(s.message).toBe("fallback");
210
+ });
211
+
212
+ it("should ignore individual fields with the wrong type", () => {
213
+ class Bad extends Error {
214
+ toStatus() {
215
+ return {
216
+ message: 42 as unknown as string,
217
+ description: { not: "a string" } as unknown as string,
218
+ details: "also not a record" as unknown as Record<string, unknown>,
219
+ };
220
+ }
221
+ }
222
+ const s = status.fromException(new Bad("fallback"));
223
+ expect(s.message).toBe("fallback");
224
+ expect(s.description).toBeUndefined();
225
+ });
226
+
227
+ it("should accept a partial return with only some valid fields", () => {
228
+ class Partial extends Error {
229
+ toStatus() {
230
+ return { message: "only headline" };
231
+ }
232
+ }
233
+ const s = status.fromException(new Partial("raw"));
234
+ expect(s.message).toBe("only headline");
235
+ expect(s.description).toBeUndefined();
236
+ });
237
+
238
+ it("should ignore toStatus if it is not a function", () => {
239
+ const err = new Error("fallback") as Error & { toStatus: string };
240
+ err.toStatus = "not a function";
241
+ const s = status.fromException(err);
242
+ expect(s.message).toBe("fallback");
243
+ });
244
+ });
132
245
  });
133
246
 
134
247
  describe("toString", () => {
@@ -301,5 +414,38 @@ describe("status", () => {
301
414
  const result = status.toString(s);
302
415
  expect(result).toContain("cat");
303
416
  });
417
+
418
+ describe("custom errors (toStatus)", () => {
419
+ class CustomError extends Error implements status.Custom {
420
+ constructor() {
421
+ super("raw");
422
+ this.name = "CustomError";
423
+ }
424
+
425
+ toStatus(): CustomCrude {
426
+ return {
427
+ message: "custom headline",
428
+ description: "line one\nline two\nline three",
429
+ details: { code: 42 },
430
+ };
431
+ }
432
+ }
433
+
434
+ it("should render the toStatus headline as the status message", () => {
435
+ const s = status.fromException(new CustomError());
436
+ expect(status.toString(s)).toContain("ERROR: custom headline");
437
+ });
438
+
439
+ it("should render a multi-line description on its own block", () => {
440
+ const out = status.toString(status.fromException(new CustomError()));
441
+ expect(out).toContain("Description:\nline one\nline two\nline three");
442
+ });
443
+
444
+ it("should include merged details under the Details section", () => {
445
+ const out = status.toString(status.fromException(new CustomError()));
446
+ expect(out).toContain("Details:");
447
+ expect(out).toContain('"code": 42');
448
+ });
449
+ });
304
450
  });
305
451
  });
@@ -13,6 +13,7 @@ import { id } from "@/id";
13
13
  import { narrow } from "@/narrow";
14
14
  import { type optional } from "@/optional";
15
15
  import { primitive } from "@/primitive";
16
+ import { record } from "@/record";
16
17
  import { type Status, type Variant } from "@/status/types.gen";
17
18
  import { TimeStamp } from "@/telem";
18
19
 
@@ -32,22 +33,68 @@ export type Crude<
32
33
  > = optional.Optional<Base<V>, "key" | "time" | "name"> &
33
34
  ([DetailsSchema] extends [z.ZodNever] ? {} : { details: z.output<DetailsSchema> });
34
35
 
35
- export const exceptionDetailsSchema = z.object({
36
- stack: z.string(),
37
- error: z.instanceof(Error),
36
+ /**
37
+ * Interface that errors may optionally implement to provide richer rendering when
38
+ * passed to {@link fromException}. Implementers return a partial {@link Crude} spec
39
+ * whose fields override the defaults derived from the underlying `Error`.
40
+ *
41
+ * This is a duck-typed contract: `fromException` checks for the presence of a
42
+ * `toStatus` method via the `in` operator, so there is no need to import this
43
+ * interface to use it.
44
+ */
45
+ export interface Custom {
46
+ toStatus(): Partial<Crude<z.ZodRecord, "error">>;
47
+ }
48
+
49
+ const customReturnZ = z.object({
50
+ message: z.string().optional(),
51
+ description: z.string().optional(),
52
+ details: record.unknownZ().optional(),
38
53
  });
39
54
 
55
+ const hasToStatusMethod = (exc: Error): exc is Error & { toStatus: () => unknown } =>
56
+ "toStatus" in exc && typeof (exc as { toStatus: unknown }).toStatus === "function";
57
+
58
+ const safeToStatus = (exc: Error): z.infer<typeof customReturnZ> | undefined => {
59
+ if (!hasToStatusMethod(exc)) return undefined;
60
+ let raw: unknown;
61
+ try {
62
+ raw = exc.toStatus();
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ const parsed = customReturnZ.safeParse(raw);
67
+ return parsed.success ? parsed.data : undefined;
68
+ };
69
+
70
+ export const exceptionDetailsSchema = z
71
+ .object({
72
+ stack: z.string(),
73
+ error: z.instanceof(Error),
74
+ })
75
+ .and(record.unknownZ());
76
+
40
77
  export const fromException = (
41
78
  exc: unknown,
42
79
  message?: string,
43
80
  ): Status<typeof exceptionDetailsSchema, z.ZodLiteral<"error">> => {
44
81
  if (!(exc instanceof Error)) throw exc;
45
- return create<typeof exceptionDetailsSchema, "error">({
82
+ const crude: Crude<typeof exceptionDetailsSchema, "error"> = {
46
83
  variant: "error",
47
84
  message: message ?? exc.message,
48
85
  description: message != null ? exc.message : undefined,
49
86
  details: { stack: exc.stack ?? "", error: exc },
50
- });
87
+ };
88
+ const custom = safeToStatus(exc);
89
+ if (custom != null) {
90
+ if (message != null && custom.message != null)
91
+ crude.message = `${message}: ${custom.message}`;
92
+ else if (custom.message != null) crude.message = custom.message;
93
+ if (custom.description != null) crude.description = custom.description;
94
+ if (custom.details != null && crude.details != null)
95
+ crude.details = { ...crude.details, ...custom.details };
96
+ }
97
+ return create<typeof exceptionDetailsSchema, "error">(crude);
51
98
  };
52
99
 
53
100
  export const create = <
@@ -97,6 +144,16 @@ const DEFAULT_TO_STRING_OPTIONS: ToStringOptions = {
97
144
  includeName: true,
98
145
  };
99
146
 
147
+ const renderDescription = (description: string): string => {
148
+ if (description.includes("\n")) return `Description:\n${description}`;
149
+ try {
150
+ const parsed = JSON.parse(description);
151
+ return `Description:\n${JSON.stringify(parsed, null, 2)}`;
152
+ } catch {
153
+ return `Description: ${description}`;
154
+ }
155
+ };
156
+
100
157
  export const toString = <Details extends z.ZodType = z.ZodNever>(
101
158
  stat: Status<Details>,
102
159
  options: ToStringOptions = {},
@@ -108,21 +165,11 @@ export const toString = <Details extends z.ZodType = z.ZodNever>(
108
165
  header += `: ${stat.message}`;
109
166
  if (opts.includeTimestamp) header += ` (${stat.time.toString("dateTime", "local")})`;
110
167
  parts.push(header);
111
- if (stat.description != null) {
112
- let descriptionText: string;
113
- try {
114
- const parsed = JSON.parse(stat.description);
115
- descriptionText = `Description:\n${JSON.stringify(parsed, null, 2)}`;
116
- } catch {
117
- descriptionText = `Description: ${stat.description}`;
118
- }
119
- parts.push(descriptionText);
120
- }
168
+ if (stat.description != null) parts.push(renderDescription(stat.description));
121
169
  if ("details" in stat && narrow.isObject(stat.details)) {
122
170
  const details = stat.details as Record<string, unknown>;
123
- // Extract stack trace separately for special formatting
124
- if ("stack" in details) parts.push(`Stack Trace:\n${String(details.stack)}`);
125
- // Include other details (excluding stack and error which don't serialize well)
171
+ if ("stack" in details && typeof details.stack === "string" && details.stack !== "")
172
+ parts.push(`Stack Trace:\n${details.stack}`);
126
173
  const extraDetails = Object.fromEntries(
127
174
  Object.entries(details).filter(([k]) => k !== "stack" && k !== "error"),
128
175
  );
@@ -8,6 +8,7 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  export * from "@/zod/nullToUndefined";
11
+ export * from "@/zod/parse";
11
12
  export * from "@/zod/schemas";
12
13
  export * from "@/zod/toArray";
13
14
  export * from "@/zod/util";