- 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
456 lines
17 KiB
JavaScript
456 lines
17 KiB
JavaScript
import { arrayEquals, flatMorph, includes, inferred, omit, throwInternalError, throwParseError } from "@ark/util";
|
|
import { mergeToJsonSchemaConfigs } from "../config.js";
|
|
import { throwInvalidOperandError } from "../constraint.js";
|
|
import { BaseNode } from "../node.js";
|
|
import { Disjoint, writeUnsatisfiableExpressionError } from "../shared/disjoint.js";
|
|
import { ArkErrors } from "../shared/errors.js";
|
|
import { structuralKinds } from "../shared/implement.js";
|
|
import { intersectNodesRoot, pipeNodesRoot } from "../shared/intersections.js";
|
|
import { $ark } from "../shared/registry.js";
|
|
import { arkKind, hasArkKind } from "../shared/utils.js";
|
|
import { assertDefaultValueAssignability } from "../structure/optional.js";
|
|
export class BaseRoot extends BaseNode {
|
|
constructor(attachments, $) {
|
|
super(attachments, $);
|
|
// define as a getter to avoid it being enumerable/spreadable
|
|
Object.defineProperty(this, arkKind, { value: "root", enumerable: false });
|
|
}
|
|
// doesn't seem possible to override this at a type-level (e.g. via declare)
|
|
// without TS complaining about getters
|
|
get rawIn() {
|
|
return super.rawIn;
|
|
}
|
|
get rawOut() {
|
|
return super.rawOut;
|
|
}
|
|
get internal() {
|
|
return this;
|
|
}
|
|
get "~standard"() {
|
|
return {
|
|
vendor: "arktype",
|
|
version: 1,
|
|
validate: input => {
|
|
const out = this(input);
|
|
if (out instanceof ArkErrors)
|
|
return out;
|
|
return { value: out };
|
|
},
|
|
jsonSchema: {
|
|
input: opts => this.rawIn.toJsonSchema({
|
|
target: validateStandardJsonSchemaTarget(opts.target),
|
|
...opts.libraryOptions
|
|
}),
|
|
output: opts => this.rawOut.toJsonSchema({
|
|
target: validateStandardJsonSchemaTarget(opts.target),
|
|
...opts.libraryOptions
|
|
})
|
|
}
|
|
};
|
|
}
|
|
as() {
|
|
return this;
|
|
}
|
|
brand(name) {
|
|
if (name === "")
|
|
return throwParseError(emptyBrandNameMessage);
|
|
return this;
|
|
}
|
|
readonly() {
|
|
return this;
|
|
}
|
|
branches = this.hasKind("union") ? this.inner.branches : [this];
|
|
distribute(mapBranch, reduceMapped) {
|
|
const mappedBranches = this.branches.map(mapBranch);
|
|
return reduceMapped?.(mappedBranches) ?? mappedBranches;
|
|
}
|
|
get shortDescription() {
|
|
return this.meta.description ?? this.defaultShortDescription;
|
|
}
|
|
toJsonSchema(opts = {}) {
|
|
const ctx = mergeToJsonSchemaConfigs(this.$.resolvedConfig.toJsonSchema, opts);
|
|
ctx.useRefs ||= this.isCyclic;
|
|
// ensure $schema is the first key if present
|
|
const schema = typeof ctx.dialect === "string" ? { $schema: ctx.dialect } : {};
|
|
Object.assign(schema, this.toJsonSchemaRecurse(ctx));
|
|
if (ctx.useRefs) {
|
|
const defs = flatMorph(this.references, (i, ref) => ref.isRoot() && !ref.alwaysExpandJsonSchema ?
|
|
[ref.id, ref.toResolvedJsonSchema(ctx)]
|
|
: []);
|
|
// draft-2020-12 uses $defs, draft-07 uses definitions
|
|
if (ctx.target === "draft-07")
|
|
Object.assign(schema, { definitions: defs });
|
|
else
|
|
schema.$defs = defs;
|
|
}
|
|
return schema;
|
|
}
|
|
toJsonSchemaRecurse(ctx) {
|
|
if (ctx.useRefs && !this.alwaysExpandJsonSchema) {
|
|
// draft-2020-12 uses $defs, draft-07 uses definitions
|
|
const defsKey = ctx.target === "draft-07" ? "definitions" : "$defs";
|
|
return { $ref: `#/${defsKey}/${this.id}` };
|
|
}
|
|
return this.toResolvedJsonSchema(ctx);
|
|
}
|
|
get alwaysExpandJsonSchema() {
|
|
return (this.isBasis() ||
|
|
this.kind === "alias" ||
|
|
(this.hasKind("union") && this.isBoolean));
|
|
}
|
|
toResolvedJsonSchema(ctx) {
|
|
const result = this.innerToJsonSchema(ctx);
|
|
return Object.assign(result, this.metaJson);
|
|
}
|
|
intersect(r) {
|
|
const rNode = this.$.parseDefinition(r);
|
|
const result = this.rawIntersect(rNode);
|
|
if (result instanceof Disjoint)
|
|
return result;
|
|
return this.$.finalize(result);
|
|
}
|
|
rawIntersect(r) {
|
|
return intersectNodesRoot(this, r, this.$);
|
|
}
|
|
toNeverIfDisjoint() {
|
|
return this;
|
|
}
|
|
and(r) {
|
|
const result = this.intersect(r);
|
|
return result instanceof Disjoint ? result.throw() : result;
|
|
}
|
|
rawAnd(r) {
|
|
const result = this.rawIntersect(r);
|
|
return result instanceof Disjoint ? result.throw() : result;
|
|
}
|
|
or(r) {
|
|
const rNode = this.$.parseDefinition(r);
|
|
return this.$.finalize(this.rawOr(rNode));
|
|
}
|
|
rawOr(r) {
|
|
const branches = [...this.branches, ...r.branches];
|
|
return this.$.node("union", branches);
|
|
}
|
|
map(flatMapEntry) {
|
|
return this.$.schema(this.applyStructuralOperation("map", [flatMapEntry]));
|
|
}
|
|
pick(...keys) {
|
|
return this.$.schema(this.applyStructuralOperation("pick", keys));
|
|
}
|
|
omit(...keys) {
|
|
return this.$.schema(this.applyStructuralOperation("omit", keys));
|
|
}
|
|
required() {
|
|
return this.$.schema(this.applyStructuralOperation("required", []));
|
|
}
|
|
partial() {
|
|
return this.$.schema(this.applyStructuralOperation("partial", []));
|
|
}
|
|
_keyof;
|
|
keyof() {
|
|
if (this._keyof)
|
|
return this._keyof;
|
|
const result = this.applyStructuralOperation("keyof", []).reduce((result, branch) => result.intersect(branch).toNeverIfDisjoint(), $ark.intrinsic.unknown.internal);
|
|
if (result.branches.length === 0) {
|
|
throwParseError(writeUnsatisfiableExpressionError(`keyof ${this.expression}`));
|
|
}
|
|
return (this._keyof = this.$.finalize(result));
|
|
}
|
|
get props() {
|
|
if (this.branches.length !== 1)
|
|
return throwParseError(writeLiteralUnionEntriesMessage(this.expression));
|
|
return [...this.applyStructuralOperation("props", [])[0]];
|
|
}
|
|
merge(r) {
|
|
const rNode = this.$.parseDefinition(r);
|
|
return this.$.schema(rNode.distribute(branch => this.applyStructuralOperation("merge", [
|
|
structureOf(branch) ??
|
|
throwParseError(writeNonStructuralOperandMessage("merge", branch.expression))
|
|
])));
|
|
}
|
|
applyStructuralOperation(operation, args) {
|
|
return this.distribute(branch => {
|
|
if (branch.equals($ark.intrinsic.object) && operation !== "merge")
|
|
// ideally this wouldn't be a special case, but for now it
|
|
// allows us to bypass `assertHasKeys` checks on base
|
|
// instantiations of generics like Pick and Omit. Could
|
|
// potentially be removed once constraints can reference each other:
|
|
// https://github.com/arktypeio/arktype/issues/1053
|
|
return branch;
|
|
const structure = structureOf(branch);
|
|
if (!structure) {
|
|
throwParseError(writeNonStructuralOperandMessage(operation, branch.expression));
|
|
}
|
|
if (operation === "keyof")
|
|
return structure.keyof();
|
|
if (operation === "get")
|
|
return structure.get(...args);
|
|
if (operation === "props")
|
|
return structure.props;
|
|
const structuralMethodName = operation === "required" ? "require"
|
|
: operation === "partial" ? "optionalize"
|
|
: operation;
|
|
return this.$.node("intersection", {
|
|
domain: "object",
|
|
structure: structure[structuralMethodName](...args)
|
|
});
|
|
});
|
|
}
|
|
get(...path) {
|
|
if (path[0] === undefined)
|
|
return this;
|
|
return this.$.schema(this.applyStructuralOperation("get", path));
|
|
}
|
|
extract(r) {
|
|
const rNode = this.$.parseDefinition(r);
|
|
return this.$.schema(this.branches.filter(branch => branch.extends(rNode)));
|
|
}
|
|
exclude(r) {
|
|
const rNode = this.$.parseDefinition(r);
|
|
return this.$.schema(this.branches.filter(branch => !branch.extends(rNode)));
|
|
}
|
|
array() {
|
|
return this.$.schema(this.isUnknown() ?
|
|
{ proto: Array }
|
|
: {
|
|
proto: Array,
|
|
sequence: this
|
|
}, { prereduced: true });
|
|
}
|
|
overlaps(r) {
|
|
const intersection = this.intersect(r);
|
|
return !(intersection instanceof Disjoint);
|
|
}
|
|
extends(r) {
|
|
if (this.isNever())
|
|
return true;
|
|
const intersection = this.intersect(r);
|
|
return (!(intersection instanceof Disjoint) && this.equals(intersection));
|
|
}
|
|
ifExtends(r) {
|
|
return this.extends(r) ? this : undefined;
|
|
}
|
|
subsumes(r) {
|
|
const rNode = this.$.parseDefinition(r);
|
|
return rNode.extends(this);
|
|
}
|
|
configure(meta, selector = "shallow") {
|
|
return this.configureReferences(meta, selector);
|
|
}
|
|
describe(description, selector = "shallow") {
|
|
return this.configure({ description }, selector);
|
|
}
|
|
// these should ideally be implemented in arktype since they use its syntax
|
|
// https://github.com/arktypeio/arktype/issues/1223
|
|
optional() {
|
|
return [this, "?"];
|
|
}
|
|
// these should ideally be implemented in arktype since they use its syntax
|
|
// https://github.com/arktypeio/arktype/issues/1223
|
|
default(thunkableValue) {
|
|
assertDefaultValueAssignability(this, thunkableValue, null);
|
|
return [this, "=", thunkableValue];
|
|
}
|
|
from(input) {
|
|
// ideally we might not validate here but for now we need to do determine
|
|
// which morphs to apply
|
|
return this.assert(input);
|
|
}
|
|
_pipe(...morphs) {
|
|
const result = morphs.reduce((acc, morph) => acc.rawPipeOnce(morph), this);
|
|
return this.$.finalize(result);
|
|
}
|
|
tryPipe(...morphs) {
|
|
const result = morphs.reduce((acc, morph) => acc.rawPipeOnce(hasArkKind(morph, "root") ? morph : ((In, ctx) => {
|
|
try {
|
|
return morph(In, ctx);
|
|
}
|
|
catch (e) {
|
|
return ctx.error({
|
|
code: "predicate",
|
|
predicate: morph,
|
|
actual: `aborted due to error:\n ${e}\n`
|
|
});
|
|
}
|
|
})), this);
|
|
return this.$.finalize(result);
|
|
}
|
|
pipe = Object.assign(this._pipe.bind(this), {
|
|
try: this.tryPipe.bind(this)
|
|
});
|
|
to(def) {
|
|
return this.$.finalize(this.toNode(this.$.parseDefinition(def)));
|
|
}
|
|
toNode(root) {
|
|
const result = pipeNodesRoot(this, root, this.$);
|
|
if (result instanceof Disjoint)
|
|
return result.throw();
|
|
return result;
|
|
}
|
|
rawPipeOnce(morph) {
|
|
if (hasArkKind(morph, "root"))
|
|
return this.toNode(morph);
|
|
return this.distribute(branch => branch.hasKind("morph") ?
|
|
this.$.node("morph", {
|
|
in: branch.inner.in,
|
|
morphs: [...branch.morphs, morph]
|
|
})
|
|
: this.$.node("morph", {
|
|
in: branch,
|
|
morphs: [morph]
|
|
}), this.$.parseSchema);
|
|
}
|
|
narrow(predicate) {
|
|
return this.constrainOut("predicate", predicate);
|
|
}
|
|
constrain(kind, schema) {
|
|
return this._constrain("root", kind, schema);
|
|
}
|
|
constrainIn(kind, schema) {
|
|
return this._constrain("in", kind, schema);
|
|
}
|
|
constrainOut(kind, schema) {
|
|
return this._constrain("out", kind, schema);
|
|
}
|
|
_constrain(io, kind, schema) {
|
|
const constraint = this.$.node(kind, schema);
|
|
if (constraint.isRoot()) {
|
|
// if the node reduces to `unknown`, nothing to do (e.g. minLength: 0)
|
|
return constraint.isUnknown() ? this : (throwInternalError(`Unexpected constraint node ${constraint}`));
|
|
}
|
|
const operand = io === "root" ? this
|
|
: io === "in" ? this.rawIn
|
|
: this.rawOut;
|
|
if (operand.hasKind("morph") ||
|
|
(constraint.impliedBasis && !operand.extends(constraint.impliedBasis))) {
|
|
return throwInvalidOperandError(kind, constraint.impliedBasis, this);
|
|
}
|
|
const partialIntersection = this.$.node("intersection", {
|
|
// important this is constraint.kind instead of kind in case
|
|
// the node was reduced during parsing
|
|
[constraint.kind]: constraint
|
|
});
|
|
const result = io === "out" ?
|
|
pipeNodesRoot(this, partialIntersection, this.$)
|
|
: intersectNodesRoot(this, partialIntersection, this.$);
|
|
if (result instanceof Disjoint)
|
|
result.throw();
|
|
return this.$.finalize(result);
|
|
}
|
|
onUndeclaredKey(cfg) {
|
|
const rule = typeof cfg === "string" ? cfg : cfg.rule;
|
|
const deep = typeof cfg === "string" ? false : cfg.deep;
|
|
return this.$.finalize(this.transform((kind, inner) => kind === "structure" ?
|
|
rule === "ignore" ?
|
|
omit(inner, { undeclared: 1 })
|
|
: { ...inner, undeclared: rule }
|
|
: inner, deep ? undefined : ({ shouldTransform: node => !includes(structuralKinds, node.kind) })));
|
|
}
|
|
hasEqualMorphs(r) {
|
|
if (!this.includesTransform && !r.includesTransform)
|
|
return true;
|
|
if (!arrayEquals(this.shallowMorphs, r.shallowMorphs))
|
|
return false;
|
|
if (!arrayEquals(this.flatMorphs, r.flatMorphs, {
|
|
isEqual: (l, r) => l.propString === r.propString &&
|
|
(l.node.hasKind("morph") && r.node.hasKind("morph") ?
|
|
l.node.hasEqualMorphs(r.node)
|
|
: l.node.hasKind("intersection") && r.node.hasKind("intersection") ?
|
|
l.node.structure?.structuralMorphRef ===
|
|
r.node.structure?.structuralMorphRef
|
|
: false)
|
|
}))
|
|
return false;
|
|
return true;
|
|
}
|
|
onDeepUndeclaredKey(behavior) {
|
|
return this.onUndeclaredKey({ rule: behavior, deep: true });
|
|
}
|
|
filter(predicate) {
|
|
return this.constrainIn("predicate", predicate);
|
|
}
|
|
divisibleBy(schema) {
|
|
return this.constrain("divisor", schema);
|
|
}
|
|
matching(schema) {
|
|
return this.constrain("pattern", schema);
|
|
}
|
|
atLeast(schema) {
|
|
return this.constrain("min", schema);
|
|
}
|
|
atMost(schema) {
|
|
return this.constrain("max", schema);
|
|
}
|
|
moreThan(schema) {
|
|
return this.constrain("min", exclusivizeRangeSchema(schema));
|
|
}
|
|
lessThan(schema) {
|
|
return this.constrain("max", exclusivizeRangeSchema(schema));
|
|
}
|
|
atLeastLength(schema) {
|
|
return this.constrain("minLength", schema);
|
|
}
|
|
atMostLength(schema) {
|
|
return this.constrain("maxLength", schema);
|
|
}
|
|
moreThanLength(schema) {
|
|
return this.constrain("minLength", exclusivizeRangeSchema(schema));
|
|
}
|
|
lessThanLength(schema) {
|
|
return this.constrain("maxLength", exclusivizeRangeSchema(schema));
|
|
}
|
|
exactlyLength(schema) {
|
|
return this.constrain("exactLength", schema);
|
|
}
|
|
atOrAfter(schema) {
|
|
return this.constrain("after", schema);
|
|
}
|
|
atOrBefore(schema) {
|
|
return this.constrain("before", schema);
|
|
}
|
|
laterThan(schema) {
|
|
return this.constrain("after", exclusivizeRangeSchema(schema));
|
|
}
|
|
earlierThan(schema) {
|
|
return this.constrain("before", exclusivizeRangeSchema(schema));
|
|
}
|
|
}
|
|
export const emptyBrandNameMessage = `Expected a non-empty brand name after #`;
|
|
const supportedJsonSchemaTargets = [
|
|
"draft-2020-12",
|
|
"draft-07"
|
|
];
|
|
export const writeInvalidJsonSchemaTargetMessage = (target) => `JSONSchema target '${target}' is not supported (must be ${supportedJsonSchemaTargets.map(t => `"${t}"`).join(" or ")})`;
|
|
const validateStandardJsonSchemaTarget = (target) => {
|
|
if (!includes(supportedJsonSchemaTargets, target))
|
|
throwParseError(writeInvalidJsonSchemaTargetMessage(target));
|
|
return target;
|
|
};
|
|
export const exclusivizeRangeSchema = (schema) => (typeof schema === "object" && !(schema instanceof Date) ?
|
|
{ ...schema, exclusive: true }
|
|
: {
|
|
rule: schema,
|
|
exclusive: true
|
|
});
|
|
export const typeOrTermExtends = (t, base) => hasArkKind(base, "root") ?
|
|
hasArkKind(t, "root") ? t.extends(base)
|
|
: base.allows(t)
|
|
: hasArkKind(t, "root") ? t.hasUnit(base)
|
|
: base === t;
|
|
const structureOf = (branch) => {
|
|
if (branch.hasKind("morph"))
|
|
return null;
|
|
if (branch.hasKind("intersection")) {
|
|
return (branch.inner.structure ??
|
|
(branch.basis?.domain === "object" ?
|
|
branch.$.bindReference($ark.intrinsic.emptyStructure)
|
|
: null));
|
|
}
|
|
if (branch.isBasis() && branch.domain === "object")
|
|
return branch.$.bindReference($ark.intrinsic.emptyStructure);
|
|
return null;
|
|
};
|
|
export const writeLiteralUnionEntriesMessage = (expression) => `Props cannot be extracted from a union. Use .distribute to extract props from each branch instead. Received:
|
|
${expression}`;
|
|
export const writeNonStructuralOperandMessage = (operation, operand) => `${operation} operand must be an object (was ${operand})`;
|