- 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
293 lines
11 KiB
JavaScript
293 lines
11 KiB
JavaScript
import { flatMorph, hasDomain, includes, isEmptyObject, isKeyOf, throwParseError } from "@ark/util";
|
|
import { constraintKeyParser, flattenConstraints, intersectConstraints } from "../constraint.js";
|
|
import { Disjoint } from "../shared/disjoint.js";
|
|
import { implementNode, prestructuralKinds, structureKeys } from "../shared/implement.js";
|
|
import { intersectOrPipeNodes } from "../shared/intersections.js";
|
|
import { hasArkKind, isNode } from "../shared/utils.js";
|
|
import { BaseRoot } from "./root.js";
|
|
import { defineRightwardIntersections } from "./utils.js";
|
|
const implementation = implementNode({
|
|
kind: "intersection",
|
|
hasAssociatedError: true,
|
|
normalize: rawSchema => {
|
|
if (isNode(rawSchema))
|
|
return rawSchema;
|
|
const { structure, ...schema } = rawSchema;
|
|
const hasRootStructureKey = !!structure;
|
|
const normalizedStructure = structure ?? {};
|
|
const normalized = flatMorph(schema, (k, v) => {
|
|
if (isKeyOf(k, structureKeys)) {
|
|
if (hasRootStructureKey) {
|
|
throwParseError(`Flattened structure key ${k} cannot be specified alongside a root 'structure' key.`);
|
|
}
|
|
normalizedStructure[k] = v;
|
|
return [];
|
|
}
|
|
return [k, v];
|
|
});
|
|
if (hasArkKind(normalizedStructure, "constraint") ||
|
|
!isEmptyObject(normalizedStructure))
|
|
normalized.structure = normalizedStructure;
|
|
return normalized;
|
|
},
|
|
finalizeInnerJson: ({ structure, ...rest }) => hasDomain(structure, "object") ? { ...structure, ...rest } : rest,
|
|
keys: {
|
|
domain: {
|
|
child: true,
|
|
parse: (schema, ctx) => ctx.$.node("domain", schema)
|
|
},
|
|
proto: {
|
|
child: true,
|
|
parse: (schema, ctx) => ctx.$.node("proto", schema)
|
|
},
|
|
structure: {
|
|
child: true,
|
|
parse: (schema, ctx) => ctx.$.node("structure", schema),
|
|
serialize: node => {
|
|
if (!node.sequence?.minLength)
|
|
return node.collapsibleJson;
|
|
const { sequence, ...structureJson } = node.collapsibleJson;
|
|
const { minVariadicLength, ...sequenceJson } = sequence;
|
|
const collapsibleSequenceJson = sequenceJson.variadic && Object.keys(sequenceJson).length === 1 ?
|
|
sequenceJson.variadic
|
|
: sequenceJson;
|
|
return { ...structureJson, sequence: collapsibleSequenceJson };
|
|
}
|
|
},
|
|
divisor: {
|
|
child: true,
|
|
parse: constraintKeyParser("divisor")
|
|
},
|
|
max: {
|
|
child: true,
|
|
parse: constraintKeyParser("max")
|
|
},
|
|
min: {
|
|
child: true,
|
|
parse: constraintKeyParser("min")
|
|
},
|
|
maxLength: {
|
|
child: true,
|
|
parse: constraintKeyParser("maxLength")
|
|
},
|
|
minLength: {
|
|
child: true,
|
|
parse: constraintKeyParser("minLength")
|
|
},
|
|
exactLength: {
|
|
child: true,
|
|
parse: constraintKeyParser("exactLength")
|
|
},
|
|
before: {
|
|
child: true,
|
|
parse: constraintKeyParser("before")
|
|
},
|
|
after: {
|
|
child: true,
|
|
parse: constraintKeyParser("after")
|
|
},
|
|
pattern: {
|
|
child: true,
|
|
parse: constraintKeyParser("pattern")
|
|
},
|
|
predicate: {
|
|
child: true,
|
|
parse: constraintKeyParser("predicate")
|
|
}
|
|
},
|
|
// leverage reduction logic from intersection and identity to ensure initial
|
|
// parse result is reduced
|
|
reduce: (inner, $) =>
|
|
// we cast union out of the result here since that only occurs when intersecting two sequences
|
|
// that cannot occur when reducing a single intersection schema using unknown
|
|
intersectIntersections({}, inner, {
|
|
$,
|
|
invert: false,
|
|
pipe: false
|
|
}),
|
|
defaults: {
|
|
description: node => {
|
|
if (node.children.length === 0)
|
|
return "unknown";
|
|
if (node.structure)
|
|
return node.structure.description;
|
|
const childDescriptions = [];
|
|
if (node.basis &&
|
|
!node.prestructurals.some(r => r.impl.obviatesBasisDescription))
|
|
childDescriptions.push(node.basis.description);
|
|
if (node.prestructurals.length) {
|
|
const sortedRefinementDescriptions = node.prestructurals
|
|
.slice()
|
|
// override alphabetization to describe min before max
|
|
.sort((l, r) => (l.kind === "min" && r.kind === "max" ? -1 : 0))
|
|
.map(r => r.description);
|
|
childDescriptions.push(...sortedRefinementDescriptions);
|
|
}
|
|
if (node.inner.predicate) {
|
|
childDescriptions.push(...node.inner.predicate.map(p => p.description));
|
|
}
|
|
return childDescriptions.join(" and ");
|
|
},
|
|
expected: source => ` ◦ ${source.errors.map(e => e.expected).join("\n ◦ ")}`,
|
|
problem: ctx => `(${ctx.actual}) must be...\n${ctx.expected}`
|
|
},
|
|
intersections: {
|
|
intersection: (l, r, ctx) => intersectIntersections(l.inner, r.inner, ctx),
|
|
...defineRightwardIntersections("intersection", (l, r, ctx) => {
|
|
// if l is unknown, return r
|
|
if (l.children.length === 0)
|
|
return r;
|
|
const { domain, proto, ...lInnerConstraints } = l.inner;
|
|
const lBasis = proto ?? domain;
|
|
const basis = lBasis ? intersectOrPipeNodes(lBasis, r, ctx) : r;
|
|
return (basis instanceof Disjoint ? basis
|
|
: l?.basis?.equals(basis) ?
|
|
// if the basis doesn't change, return the original intesection
|
|
l
|
|
// given we've already precluded l being unknown, the result must
|
|
// be an intersection with the new basis result integrated
|
|
: l.$.node("intersection", { ...lInnerConstraints, [basis.kind]: basis }, { prereduced: true }));
|
|
})
|
|
}
|
|
});
|
|
export class IntersectionNode extends BaseRoot {
|
|
basis = this.inner.domain ?? this.inner.proto ?? null;
|
|
prestructurals = [];
|
|
refinements = this.children.filter((node) => {
|
|
if (!node.isRefinement())
|
|
return false;
|
|
if (includes(prestructuralKinds, node.kind))
|
|
// mutation is fine during initialization
|
|
this.prestructurals.push(node);
|
|
return true;
|
|
});
|
|
structure = this.inner.structure;
|
|
expression = writeIntersectionExpression(this);
|
|
get shallowMorphs() {
|
|
return this.inner.structure?.structuralMorph ?
|
|
[this.inner.structure.structuralMorph]
|
|
: [];
|
|
}
|
|
get defaultShortDescription() {
|
|
return this.basis?.defaultShortDescription ?? "present";
|
|
}
|
|
innerToJsonSchema(ctx) {
|
|
return this.children.reduce(
|
|
// cast is required since TS doesn't know children have compatible schema prerequisites
|
|
(schema, child) => child.isBasis() ?
|
|
child.toJsonSchemaRecurse(ctx)
|
|
: child.reduceJsonSchema(schema, ctx), {});
|
|
}
|
|
traverseAllows = (data, ctx) => this.children.every(child => child.traverseAllows(data, ctx));
|
|
traverseApply = (data, ctx) => {
|
|
const errorCount = ctx.currentErrorCount;
|
|
if (this.basis) {
|
|
this.basis.traverseApply(data, ctx);
|
|
if (ctx.currentErrorCount > errorCount)
|
|
return;
|
|
}
|
|
if (this.prestructurals.length) {
|
|
for (let i = 0; i < this.prestructurals.length - 1; i++) {
|
|
this.prestructurals[i].traverseApply(data, ctx);
|
|
if (ctx.failFast && ctx.currentErrorCount > errorCount)
|
|
return;
|
|
}
|
|
this.prestructurals[this.prestructurals.length - 1].traverseApply(data, ctx);
|
|
if (ctx.currentErrorCount > errorCount)
|
|
return;
|
|
}
|
|
if (this.structure) {
|
|
this.structure.traverseApply(data, ctx);
|
|
if (ctx.currentErrorCount > errorCount)
|
|
return;
|
|
}
|
|
if (this.inner.predicate) {
|
|
for (let i = 0; i < this.inner.predicate.length - 1; i++) {
|
|
this.inner.predicate[i].traverseApply(data, ctx);
|
|
if (ctx.failFast && ctx.currentErrorCount > errorCount)
|
|
return;
|
|
}
|
|
this.inner.predicate[this.inner.predicate.length - 1].traverseApply(data, ctx);
|
|
}
|
|
};
|
|
compile(js) {
|
|
if (js.traversalKind === "Allows") {
|
|
for (const child of this.children)
|
|
js.check(child);
|
|
js.return(true);
|
|
return;
|
|
}
|
|
js.initializeErrorCount();
|
|
if (this.basis) {
|
|
js.check(this.basis);
|
|
// we only have to return conditionally if this is not the last check
|
|
if (this.children.length > 1)
|
|
js.returnIfFail();
|
|
}
|
|
if (this.prestructurals.length) {
|
|
for (let i = 0; i < this.prestructurals.length - 1; i++) {
|
|
js.check(this.prestructurals[i]);
|
|
js.returnIfFailFast();
|
|
}
|
|
js.check(this.prestructurals[this.prestructurals.length - 1]);
|
|
if (this.structure || this.inner.predicate)
|
|
js.returnIfFail();
|
|
}
|
|
if (this.structure) {
|
|
js.check(this.structure);
|
|
if (this.inner.predicate)
|
|
js.returnIfFail();
|
|
}
|
|
if (this.inner.predicate) {
|
|
for (let i = 0; i < this.inner.predicate.length - 1; i++) {
|
|
js.check(this.inner.predicate[i]);
|
|
// since predicates can be chained, we have to fail immediately
|
|
// if one fails
|
|
js.returnIfFail();
|
|
}
|
|
js.check(this.inner.predicate[this.inner.predicate.length - 1]);
|
|
}
|
|
}
|
|
}
|
|
export const Intersection = {
|
|
implementation,
|
|
Node: IntersectionNode
|
|
};
|
|
const writeIntersectionExpression = (node) => {
|
|
if (node.structure?.expression)
|
|
return node.structure.expression;
|
|
const basisExpression = (node.basis &&
|
|
!node.prestructurals.some(n => n.impl.obviatesBasisExpression)) ?
|
|
node.basis.nestableExpression
|
|
: "";
|
|
const refinementsExpression = node.prestructurals
|
|
.map(n => n.expression)
|
|
.join(" & ");
|
|
const fullExpression = `${basisExpression}${basisExpression ? " " : ""}${refinementsExpression}`;
|
|
if (fullExpression === "Array == 0")
|
|
return "[]";
|
|
return fullExpression || "unknown";
|
|
};
|
|
const intersectIntersections = (l, r, ctx) => {
|
|
const baseInner = {};
|
|
const lBasis = l.proto ?? l.domain;
|
|
const rBasis = r.proto ?? r.domain;
|
|
const basisResult = lBasis ?
|
|
rBasis ?
|
|
intersectOrPipeNodes(lBasis, rBasis, ctx)
|
|
: lBasis
|
|
: rBasis;
|
|
if (basisResult instanceof Disjoint)
|
|
return basisResult;
|
|
if (basisResult)
|
|
baseInner[basisResult.kind] = basisResult;
|
|
return intersectConstraints({
|
|
kind: "intersection",
|
|
baseInner,
|
|
l: flattenConstraints(l),
|
|
r: flattenConstraints(r),
|
|
roots: [],
|
|
ctx
|
|
});
|
|
};
|