@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.
- package/.turbo/turbo-build.log +7 -7
- package/dist/src/binary/codec.d.ts.map +1 -1
- package/dist/src/color/color.d.ts +1 -1
- package/dist/src/color/palette.d.ts +2 -2
- package/dist/src/deep/atKeys.d.ts +27 -0
- package/dist/src/deep/atKeys.d.ts.map +1 -0
- package/dist/src/deep/atKeys.spec.d.ts +2 -0
- package/dist/src/deep/atKeys.spec.d.ts.map +1 -0
- package/dist/src/deep/external.d.ts +1 -0
- package/dist/src/deep/external.d.ts.map +1 -1
- package/dist/src/fmt/external.d.ts +3 -0
- package/dist/src/fmt/external.d.ts.map +1 -0
- package/dist/src/fmt/index.d.ts +2 -0
- package/dist/src/fmt/index.d.ts.map +1 -0
- package/dist/src/fmt/path.d.ts +13 -0
- package/dist/src/fmt/path.d.ts.map +1 -0
- package/dist/src/fmt/path.spec.d.ts +2 -0
- package/dist/src/fmt/path.spec.d.ts.map +1 -0
- package/dist/src/fmt/value.d.ts +23 -0
- package/dist/src/fmt/value.d.ts.map +1 -0
- package/dist/src/fmt/value.spec.d.ts +2 -0
- package/dist/src/fmt/value.spec.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/label/types.gen.d.ts +2 -2
- package/dist/src/narrow/narrow.d.ts +9 -0
- package/dist/src/narrow/narrow.d.ts.map +1 -1
- package/dist/src/primitive/primitive.d.ts +10 -0
- package/dist/src/primitive/primitive.d.ts.map +1 -1
- package/dist/src/status/status.d.ts +14 -2
- package/dist/src/status/status.d.ts.map +1 -1
- package/dist/src/status/types.gen.d.ts +1 -1
- package/dist/src/telem/series.d.ts +4 -4
- package/dist/src/telem/telem.d.ts +10 -10
- package/dist/src/zod/external.d.ts +1 -0
- package/dist/src/zod/external.d.ts.map +1 -1
- package/dist/src/zod/parse.d.ts +47 -0
- package/dist/src/zod/parse.d.ts.map +1 -0
- package/dist/src/zod/parse.spec.d.ts +2 -0
- package/dist/src/zod/parse.spec.d.ts.map +1 -0
- package/dist/x.cjs +16 -7
- package/dist/x.js +2190 -1869
- package/package.json +3 -3
- package/src/binary/codec.ts +3 -2
- package/src/deep/atKeys.spec.ts +107 -0
- package/src/deep/atKeys.ts +49 -0
- package/src/deep/external.ts +1 -0
- package/src/fmt/external.ts +11 -0
- package/src/fmt/index.ts +10 -0
- package/src/fmt/path.spec.ts +46 -0
- package/src/fmt/path.ts +30 -0
- package/src/fmt/value.spec.ts +206 -0
- package/src/fmt/value.ts +83 -0
- package/src/index.ts +1 -0
- package/src/narrow/narrow.spec.ts +43 -0
- package/src/narrow/narrow.ts +15 -0
- package/src/primitive/primitive.spec.ts +51 -0
- package/src/primitive/primitive.ts +12 -0
- package/src/status/status.spec.ts +146 -0
- package/src/status/status.ts +65 -18
- package/src/zod/external.ts +1 -0
- package/src/zod/parse.spec.ts +702 -0
- package/src/zod/parse.ts +519 -0
- 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
|
});
|
package/src/status/status.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
);
|