Files
headroom/frontend/node_modules/@ark/schema/out/shared/errors.js
Santhosh Janardhanan de2d83092e feat: Reinitialize frontend with SvelteKit and TypeScript
- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
2026-02-17 16:19:59 -05:00

253 lines
8.4 KiB
JavaScript

import { CastableBase, ReadonlyArray, ReadonlyPath, append, conflatenateAll, defineProperties, flatMorph, stringifyPath } from "@ark/util";
import { arkKind } from "./utils.js";
export class ArkError extends CastableBase {
[arkKind] = "error";
path;
data;
nodeConfig;
input;
ctx;
// TS gets confused by <code>, so internally we just use the base type for input
constructor({ prefixPath, relativePath, ...input }, ctx) {
super();
this.input = input;
this.ctx = ctx;
defineProperties(this, input);
const data = ctx.data;
if (input.code === "union") {
input.errors = input.errors.flatMap(innerError => {
// flatten union errors to avoid repeating context like "foo must be foo must be"...
const flat = innerError.hasCode("union") ? innerError.errors : [innerError];
if (!prefixPath && !relativePath)
return flat;
return flat.map(e => e.transform(e => ({
...e,
path: conflatenateAll(prefixPath, e.path, relativePath)
})));
});
}
this.nodeConfig = ctx.config[this.code];
const basePath = [...(input.path ?? ctx.path)];
if (relativePath)
basePath.push(...relativePath);
if (prefixPath)
basePath.unshift(...prefixPath);
this.path = new ReadonlyPath(...basePath);
this.data = "data" in input ? input.data : data;
}
transform(f) {
return new ArkError(f({
data: this.data,
path: this.path,
...this.input
}), this.ctx);
}
hasCode(code) {
return this.code === code;
}
get propString() {
return stringifyPath(this.path);
}
get expected() {
if (this.input.expected)
return this.input.expected;
const config = this.meta?.expected ?? this.nodeConfig.expected;
return typeof config === "function" ? config(this.input) : config;
}
get actual() {
if (this.input.actual)
return this.input.actual;
const config = this.meta?.actual ?? this.nodeConfig.actual;
return typeof config === "function" ? config(this.data) : config;
}
get problem() {
if (this.input.problem)
return this.input.problem;
const config = this.meta?.problem ?? this.nodeConfig.problem;
return typeof config === "function" ? config(this) : config;
}
get message() {
if (this.input.message)
return this.input.message;
const config = this.meta?.message ?? this.nodeConfig.message;
return typeof config === "function" ? config(this) : config;
}
get flat() {
return this.hasCode("intersection") ? [...this.errors] : [this];
}
toJSON() {
return {
data: this.data,
path: this.path,
...this.input,
expected: this.expected,
actual: this.actual,
problem: this.problem,
message: this.message
};
}
toString() {
return this.message;
}
throw() {
throw this;
}
}
/**
* A ReadonlyArray of `ArkError`s returned by a Type on invalid input.
*
* Subsequent errors added at an existing path are merged into an
* ArkError intersection.
*/
export class ArkErrors extends ReadonlyArray {
[arkKind] = "errors";
ctx;
constructor(ctx) {
super();
this.ctx = ctx;
}
/**
* Errors by a pathString representing their location.
*/
byPath = Object.create(null);
/**
* {@link byPath} flattened so that each value is an array of ArkError instances at that path.
*
* ✅ Since "intersection" errors will be flattened to their constituent `.errors`,
* they will never be directly present in this representation.
*/
get flatByPath() {
return flatMorph(this.byPath, (k, v) => [k, v.flat]);
}
/**
* {@link byPath} flattened so that each value is an array of problem strings at that path.
*/
get flatProblemsByPath() {
return flatMorph(this.byPath, (k, v) => [k, v.flat.map(e => e.problem)]);
}
/**
* All pathStrings at which errors are present mapped to the errors occuring
* at that path or any nested path within it.
*/
byAncestorPath = Object.create(null);
count = 0;
mutable = this;
/**
* Throw a TraversalError based on these errors.
*/
throw() {
throw this.toTraversalError();
}
/**
* Converts ArkErrors to TraversalError, a subclass of `Error` suitable for throwing with nice
* formatting.
*/
toTraversalError() {
return new TraversalError(this);
}
/**
* Append an ArkError to this array, ignoring duplicates.
*/
add(error) {
const existing = this.byPath[error.propString];
if (existing) {
if (error === existing)
return;
// If the existing error is an error for a value constrained to "never",
// then we don't want to intersect the error messages.
if (existing.hasCode("union") && existing.errors.length === 0)
return;
// If the new error is an error for a value constrained to "never",
// then we want to override any existing errors.
const errorIntersection = error.hasCode("union") && error.errors.length === 0 ?
error
: new ArkError({
code: "intersection",
errors: existing.hasCode("intersection") ?
[...existing.errors, error]
: [existing, error]
}, this.ctx);
const existingIndex = this.indexOf(existing);
this.mutable[existingIndex === -1 ? this.length : existingIndex] =
errorIntersection;
this.byPath[error.propString] = errorIntersection;
// add the original error here rather than the intersection
// since the intersection is reflected by the array of errors at
// this path
this.addAncestorPaths(error);
}
else {
this.byPath[error.propString] = error;
this.addAncestorPaths(error);
this.mutable.push(error);
}
this.count++;
}
transform(f) {
const result = new ArkErrors(this.ctx);
for (const e of this)
result.add(f(e));
return result;
}
/**
* Add all errors from an ArkErrors instance, ignoring duplicates and
* prefixing their paths with that of the current Traversal.
*/
merge(errors) {
for (const e of errors) {
this.add(new ArkError({ ...e, path: [...this.ctx.path, ...e.path] }, this.ctx));
}
}
/**
* @internal
*/
affectsPath(path) {
if (this.length === 0)
return false;
return (
// this would occur if there is an existing error at a prefix of path
// e.g. the path is ["foo", "bar"] and there is an error at ["foo"]
path.stringifyAncestors().some(s => s in this.byPath) ||
// this would occur if there is an existing error at a suffix of path
// e.g. the path is ["foo"] and there is an error at ["foo", "bar"]
path.stringify() in this.byAncestorPath);
}
/**
* A human-readable summary of all errors.
*/
get summary() {
return this.toString();
}
/**
* Alias of this ArkErrors instance for StandardSchema compatibility.
*/
get issues() {
return this;
}
toJSON() {
return [...this.map(e => e.toJSON())];
}
toString() {
return this.join("\n");
}
addAncestorPaths(error) {
for (const propString of error.path.stringifyAncestors()) {
this.byAncestorPath[propString] = append(this.byAncestorPath[propString], error);
}
}
}
export class TraversalError extends Error {
name = "TraversalError";
constructor(errors) {
if (errors.length === 1)
super(errors.summary);
else
super("\n" + errors.map(error => `${indent(error)}`).join("\n"));
Object.defineProperty(this, "arkErrors", {
value: errors,
enumerable: false
});
}
}
const indent = (error) => error.toString().split("\n").join("\n ");