- 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
439 lines
18 KiB
JavaScript
439 lines
18 KiB
JavaScript
import { Callable, appendUnique, flatMorph, includes, isArray, isEmptyObject, isKeyOf, liftArray, printable, stringifyPath, throwError, throwInternalError } from "@ark/util";
|
|
import { basisKinds, constraintKinds, precedenceOfKind, refinementKinds, rootKinds, structuralKinds } from "./shared/implement.js";
|
|
import { $ark } from "./shared/registry.js";
|
|
import { Traversal } from "./shared/traversal.js";
|
|
import { isNode } from "./shared/utils.js";
|
|
export class BaseNode extends Callable {
|
|
attachments;
|
|
$;
|
|
onFail;
|
|
includesTransform;
|
|
includesContextualPredicate;
|
|
isCyclic;
|
|
allowsRequiresContext;
|
|
rootApplyStrategy;
|
|
contextFreeMorph;
|
|
rootApply;
|
|
referencesById;
|
|
shallowReferences;
|
|
flatRefs;
|
|
flatMorphs;
|
|
allows;
|
|
get shallowMorphs() {
|
|
return [];
|
|
}
|
|
constructor(attachments, $) {
|
|
super((data, pipedFromCtx, onFail = this.onFail) => {
|
|
if (pipedFromCtx) {
|
|
this.traverseApply(data, pipedFromCtx);
|
|
return pipedFromCtx.hasError() ?
|
|
pipedFromCtx.errors
|
|
: pipedFromCtx.data;
|
|
}
|
|
return this.rootApply(data, onFail);
|
|
}, { attach: attachments });
|
|
this.attachments = attachments;
|
|
this.$ = $;
|
|
this.onFail = this.meta.onFail ?? this.$.resolvedConfig.onFail;
|
|
this.includesTransform =
|
|
this.hasKind("morph") ||
|
|
(this.hasKind("structure") && this.structuralMorph !== undefined) ||
|
|
(this.hasKind("sequence") && this.inner.defaultables !== undefined);
|
|
// if a predicate accepts exactly one arg, we can safely skip passing context
|
|
// technically, a predicate could be written like `(data, ...[ctx]) => ctx.mustBe("malicious")`
|
|
// that would break here, but it feels like a pathological case and is better to let people optimize
|
|
this.includesContextualPredicate =
|
|
this.hasKind("predicate") && this.inner.predicate.length !== 1;
|
|
this.isCyclic = this.kind === "alias";
|
|
this.referencesById = { [this.id]: this };
|
|
this.shallowReferences =
|
|
this.hasKind("structure") ?
|
|
[this, ...this.children]
|
|
: this.children.reduce((acc, child) => appendUniqueNodes(acc, child.shallowReferences), [this]);
|
|
const isStructural = this.isStructural();
|
|
this.flatRefs = [];
|
|
this.flatMorphs = [];
|
|
for (let i = 0; i < this.children.length; i++) {
|
|
this.includesTransform ||= this.children[i].includesTransform;
|
|
this.includesContextualPredicate ||=
|
|
this.children[i].includesContextualPredicate;
|
|
this.isCyclic ||= this.children[i].isCyclic;
|
|
if (!isStructural) {
|
|
const childFlatRefs = this.children[i].flatRefs;
|
|
for (let j = 0; j < childFlatRefs.length; j++) {
|
|
const childRef = childFlatRefs[j];
|
|
if (!this.flatRefs.some(existing => flatRefsAreEqual(existing, childRef))) {
|
|
this.flatRefs.push(childRef);
|
|
for (const branch of childRef.node.branches) {
|
|
if (branch.hasKind("morph") ||
|
|
(branch.hasKind("intersection") &&
|
|
branch.structure?.structuralMorph !== undefined)) {
|
|
this.flatMorphs.push({
|
|
path: childRef.path,
|
|
propString: childRef.propString,
|
|
node: branch
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Object.assign(this.referencesById, this.children[i].referencesById);
|
|
}
|
|
this.flatRefs.sort((l, r) => l.path.length > r.path.length ? 1
|
|
: l.path.length < r.path.length ? -1
|
|
: l.propString > r.propString ? 1
|
|
: l.propString < r.propString ? -1
|
|
: l.node.expression < r.node.expression ? -1
|
|
: 1);
|
|
this.allowsRequiresContext =
|
|
this.includesContextualPredicate || this.isCyclic;
|
|
this.rootApplyStrategy =
|
|
!this.allowsRequiresContext && this.flatMorphs.length === 0 ?
|
|
this.shallowMorphs.length === 0 ? "allows"
|
|
: (this.shallowMorphs.every(morph => morph.length === 1 || morph.name === "$arkStructuralMorph")) ?
|
|
this.hasKind("union") ?
|
|
// multiple morphs not yet supported for optimistic compilation
|
|
this.branches.some(branch => branch.shallowMorphs.length > 1) ?
|
|
"contextual"
|
|
: "branchedOptimistic"
|
|
: this.shallowMorphs.length > 1 ? "contextual"
|
|
: "optimistic"
|
|
: "contextual"
|
|
: "contextual";
|
|
this.rootApply = this.createRootApply();
|
|
this.allows =
|
|
this.allowsRequiresContext ?
|
|
data => this.traverseAllows(data, new Traversal(data, this.$.resolvedConfig))
|
|
: data => this.traverseAllows(data);
|
|
}
|
|
createRootApply() {
|
|
switch (this.rootApplyStrategy) {
|
|
case "allows":
|
|
return (data, onFail) => {
|
|
if (this.allows(data))
|
|
return data;
|
|
const ctx = new Traversal(data, this.$.resolvedConfig);
|
|
this.traverseApply(data, ctx);
|
|
return ctx.finalize(onFail);
|
|
};
|
|
case "contextual":
|
|
return (data, onFail) => {
|
|
const ctx = new Traversal(data, this.$.resolvedConfig);
|
|
this.traverseApply(data, ctx);
|
|
return ctx.finalize(onFail);
|
|
};
|
|
case "optimistic":
|
|
this.contextFreeMorph = this.shallowMorphs[0];
|
|
const clone = this.$.resolvedConfig.clone;
|
|
return (data, onFail) => {
|
|
if (this.allows(data)) {
|
|
return this.contextFreeMorph((clone &&
|
|
((typeof data === "object" && data !== null) ||
|
|
typeof data === "function")) ?
|
|
clone(data)
|
|
: data);
|
|
}
|
|
const ctx = new Traversal(data, this.$.resolvedConfig);
|
|
this.traverseApply(data, ctx);
|
|
return ctx.finalize(onFail);
|
|
};
|
|
case "branchedOptimistic":
|
|
return this.createBranchedOptimisticRootApply();
|
|
default:
|
|
this.rootApplyStrategy;
|
|
return throwInternalError(`Unexpected rootApplyStrategy ${this.rootApplyStrategy}`);
|
|
}
|
|
}
|
|
compiledMeta = compileMeta(this.metaJson);
|
|
cacheGetter(name, value) {
|
|
Object.defineProperty(this, name, { value });
|
|
return value;
|
|
}
|
|
get description() {
|
|
return this.cacheGetter("description", this.meta?.description ??
|
|
this.$.resolvedConfig[this.kind].description(this));
|
|
}
|
|
// we don't cache this currently since it can be updated once a scope finishes
|
|
// resolving cyclic references, although it may be possible to ensure it is cached safely
|
|
get references() {
|
|
return Object.values(this.referencesById);
|
|
}
|
|
precedence = precedenceOfKind(this.kind);
|
|
precompilation;
|
|
// defined as an arrow function since it is often detached, e.g. when passing to tRPC
|
|
// otherwise, would run into issues with this binding
|
|
assert = (data, pipedFromCtx) => this(data, pipedFromCtx, errors => errors.throw());
|
|
traverse(data, pipedFromCtx) {
|
|
return this(data, pipedFromCtx, null);
|
|
}
|
|
/** rawIn should be used internally instead */
|
|
get in() {
|
|
// ensure the node has been finalized if in is being used externally
|
|
return this.cacheGetter("in", this.rawIn.isRoot() ? this.$.finalize(this.rawIn) : this.rawIn);
|
|
}
|
|
get rawIn() {
|
|
return this.cacheGetter("rawIn", this.getIo("in"));
|
|
}
|
|
/** rawOut should be used internally instead */
|
|
get out() {
|
|
// ensure the node has been finalized if out is being used externally
|
|
return this.cacheGetter("out", this.rawOut.isRoot() ? this.$.finalize(this.rawOut) : this.rawOut);
|
|
}
|
|
get rawOut() {
|
|
return this.cacheGetter("rawOut", this.getIo("out"));
|
|
}
|
|
// Should be refactored to use transform
|
|
// https://github.com/arktypeio/arktype/issues/1020
|
|
getIo(ioKind) {
|
|
if (!this.includesTransform)
|
|
return this;
|
|
const ioInner = {};
|
|
for (const [k, v] of this.innerEntries) {
|
|
const keySchemaImplementation = this.impl.keys[k];
|
|
if (keySchemaImplementation.reduceIo)
|
|
keySchemaImplementation.reduceIo(ioKind, ioInner, v);
|
|
else if (keySchemaImplementation.child) {
|
|
const childValue = v;
|
|
ioInner[k] =
|
|
isArray(childValue) ?
|
|
childValue.map(child => ioKind === "in" ? child.rawIn : child.rawOut)
|
|
: ioKind === "in" ? childValue.rawIn
|
|
: childValue.rawOut;
|
|
}
|
|
else
|
|
ioInner[k] = v;
|
|
}
|
|
return this.$.node(this.kind, ioInner);
|
|
}
|
|
toJSON() {
|
|
return this.json;
|
|
}
|
|
toString() {
|
|
return `Type<${this.expression}>`;
|
|
}
|
|
equals(r) {
|
|
const rNode = isNode(r) ? r : this.$.parseDefinition(r);
|
|
return this.innerHash === rNode.innerHash;
|
|
}
|
|
ifEquals(r) {
|
|
return this.equals(r) ? this : undefined;
|
|
}
|
|
hasKind(kind) {
|
|
return this.kind === kind;
|
|
}
|
|
assertHasKind(kind) {
|
|
if (this.kind !== kind)
|
|
throwError(`${this.kind} node was not of asserted kind ${kind}`);
|
|
return this;
|
|
}
|
|
hasKindIn(...kinds) {
|
|
return kinds.includes(this.kind);
|
|
}
|
|
assertHasKindIn(...kinds) {
|
|
if (!includes(kinds, this.kind))
|
|
throwError(`${this.kind} node was not one of asserted kinds ${kinds}`);
|
|
return this;
|
|
}
|
|
isBasis() {
|
|
return includes(basisKinds, this.kind);
|
|
}
|
|
isConstraint() {
|
|
return includes(constraintKinds, this.kind);
|
|
}
|
|
isStructural() {
|
|
return includes(structuralKinds, this.kind);
|
|
}
|
|
isRefinement() {
|
|
return includes(refinementKinds, this.kind);
|
|
}
|
|
isRoot() {
|
|
return includes(rootKinds, this.kind);
|
|
}
|
|
isUnknown() {
|
|
return this.hasKind("intersection") && this.children.length === 0;
|
|
}
|
|
isNever() {
|
|
return this.hasKind("union") && this.children.length === 0;
|
|
}
|
|
hasUnit(value) {
|
|
return this.hasKind("unit") && this.allows(value);
|
|
}
|
|
hasOpenIntersection() {
|
|
return this.impl.intersectionIsOpen;
|
|
}
|
|
get nestableExpression() {
|
|
return this.expression;
|
|
}
|
|
select(selector) {
|
|
const normalized = NodeSelector.normalize(selector);
|
|
return this._select(normalized);
|
|
}
|
|
_select(selector) {
|
|
let nodes = NodeSelector.applyBoundary[selector.boundary ?? "references"](this);
|
|
if (selector.kind)
|
|
nodes = nodes.filter(n => n.kind === selector.kind);
|
|
if (selector.where)
|
|
nodes = nodes.filter(selector.where);
|
|
return NodeSelector.applyMethod[selector.method ?? "filter"](nodes, this, selector);
|
|
}
|
|
transform(mapper, opts) {
|
|
return this._transform(mapper, this._createTransformContext(opts));
|
|
}
|
|
_createTransformContext(opts) {
|
|
return {
|
|
root: this,
|
|
selected: undefined,
|
|
seen: {},
|
|
path: [],
|
|
parseOptions: {
|
|
prereduced: opts?.prereduced ?? false
|
|
},
|
|
undeclaredKeyHandling: undefined,
|
|
...opts
|
|
};
|
|
}
|
|
_transform(mapper, ctx) {
|
|
const $ = ctx.bindScope ?? this.$;
|
|
if (ctx.seen[this.id])
|
|
// Cyclic handling needs to be made more robust
|
|
// https://github.com/arktypeio/arktype/issues/944
|
|
return this.$.lazilyResolve(ctx.seen[this.id]);
|
|
if (ctx.shouldTransform?.(this, ctx) === false)
|
|
return this;
|
|
let transformedNode;
|
|
ctx.seen[this.id] = () => transformedNode;
|
|
if (this.hasKind("structure") &&
|
|
this.undeclared !== ctx.undeclaredKeyHandling) {
|
|
ctx = {
|
|
...ctx,
|
|
undeclaredKeyHandling: this.undeclared
|
|
};
|
|
}
|
|
const innerWithTransformedChildren = flatMorph(this.inner, (k, v) => {
|
|
if (!this.impl.keys[k].child)
|
|
return [k, v];
|
|
const children = v;
|
|
if (!isArray(children)) {
|
|
const transformed = children._transform(mapper, ctx);
|
|
return transformed ? [k, transformed] : [];
|
|
}
|
|
// if the value was previously explicitly set to an empty list,
|
|
// (e.g. branches for `never`), ensure it is not pruned
|
|
if (children.length === 0)
|
|
return [k, v];
|
|
const transformed = children.flatMap(n => {
|
|
const transformedChild = n._transform(mapper, ctx);
|
|
return transformedChild ?? [];
|
|
});
|
|
return transformed.length ? [k, transformed] : [];
|
|
});
|
|
delete ctx.seen[this.id];
|
|
const innerWithMeta = Object.assign(innerWithTransformedChildren, {
|
|
meta: this.meta
|
|
});
|
|
const transformedInner = ctx.selected && !ctx.selected.includes(this) ?
|
|
innerWithMeta
|
|
: mapper(this.kind, innerWithMeta, ctx);
|
|
if (transformedInner === null)
|
|
return null;
|
|
if (isNode(transformedInner))
|
|
return (transformedNode = transformedInner);
|
|
const transformedKeys = Object.keys(transformedInner);
|
|
const hasNoTypedKeys = transformedKeys.length === 0 ||
|
|
(transformedKeys.length === 1 && transformedKeys[0] === "meta");
|
|
if (hasNoTypedKeys &&
|
|
// if inner was previously an empty object (e.g. unknown) ensure it is not pruned
|
|
!isEmptyObject(this.inner))
|
|
return null;
|
|
if ((this.kind === "required" ||
|
|
this.kind === "optional" ||
|
|
this.kind === "index") &&
|
|
!("value" in transformedInner)) {
|
|
return ctx.undeclaredKeyHandling ?
|
|
{ ...transformedInner, value: $ark.intrinsic.unknown }
|
|
: null;
|
|
}
|
|
if (this.kind === "morph") {
|
|
;
|
|
transformedInner.in ??= $ark.intrinsic
|
|
.unknown;
|
|
}
|
|
return (transformedNode = $.node(this.kind, transformedInner, ctx.parseOptions));
|
|
}
|
|
configureReferences(meta, selector = "references") {
|
|
const normalized = NodeSelector.normalize(selector);
|
|
const mapper = (typeof meta === "string" ?
|
|
(kind, inner) => ({
|
|
...inner,
|
|
meta: { ...inner.meta, description: meta }
|
|
})
|
|
: typeof meta === "function" ?
|
|
(kind, inner) => ({ ...inner, meta: meta(inner.meta) })
|
|
: (kind, inner) => ({
|
|
...inner,
|
|
meta: { ...inner.meta, ...meta }
|
|
}));
|
|
if (normalized.boundary === "self") {
|
|
return this.$.node(this.kind, mapper(this.kind, { ...this.inner, meta: this.meta }));
|
|
}
|
|
const rawSelected = this._select(normalized);
|
|
const selected = rawSelected && liftArray(rawSelected);
|
|
const shouldTransform = normalized.boundary === "child" ?
|
|
(node, ctx) => ctx.root.children.includes(node)
|
|
: normalized.boundary === "shallow" ? node => node.kind !== "structure"
|
|
: () => true;
|
|
return this.$.finalize(this.transform(mapper, {
|
|
shouldTransform,
|
|
selected
|
|
}));
|
|
}
|
|
}
|
|
const NodeSelector = {
|
|
applyBoundary: {
|
|
self: node => [node],
|
|
child: node => [...node.children],
|
|
shallow: node => [...node.shallowReferences],
|
|
references: node => [...node.references]
|
|
},
|
|
applyMethod: {
|
|
filter: nodes => nodes,
|
|
assertFilter: (nodes, from, selector) => {
|
|
if (nodes.length === 0)
|
|
throwError(writeSelectAssertionMessage(from, selector));
|
|
return nodes;
|
|
},
|
|
find: nodes => nodes[0],
|
|
assertFind: (nodes, from, selector) => {
|
|
if (nodes.length === 0)
|
|
throwError(writeSelectAssertionMessage(from, selector));
|
|
return nodes[0];
|
|
}
|
|
},
|
|
normalize: (selector) => typeof selector === "function" ?
|
|
{ boundary: "references", method: "filter", where: selector }
|
|
: typeof selector === "string" ?
|
|
isKeyOf(selector, NodeSelector.applyBoundary) ?
|
|
{ method: "filter", boundary: selector }
|
|
: { boundary: "references", method: "filter", kind: selector }
|
|
: { boundary: "references", method: "filter", ...selector }
|
|
};
|
|
const writeSelectAssertionMessage = (from, selector) => `${from} had no references matching ${printable(selector)}.`;
|
|
export const typePathToPropString = (path) => stringifyPath(path, {
|
|
stringifyNonKey: node => node.expression
|
|
});
|
|
const referenceMatcher = /"(\$ark\.[^"]+)"/g;
|
|
const compileMeta = (metaJson) => JSON.stringify(metaJson).replace(referenceMatcher, "$1");
|
|
export const flatRef = (path, node) => ({
|
|
path,
|
|
node,
|
|
propString: typePathToPropString(path)
|
|
});
|
|
export const flatRefsAreEqual = (l, r) => l.propString === r.propString && l.node.equals(r.node);
|
|
export const appendUniqueFlatRefs = (existing, refs) => appendUnique(existing, refs, {
|
|
isEqual: flatRefsAreEqual
|
|
});
|
|
export const appendUniqueNodes = (existing, refs) => appendUnique(existing, refs, {
|
|
isEqual: (l, r) => l.equals(r)
|
|
});
|