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
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,41 @@
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { ToJsonSchema } from "../shared/toJsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { BaseRange, type BaseRangeInner, type LimitSchemaValue, type UnknownExpandedRangeSchema, type UnknownNormalizedRangeSchema } from "./range.ts";
export declare namespace After {
interface Inner extends BaseRangeInner {
rule: Date;
}
interface NormalizedSchema extends UnknownNormalizedRangeSchema {
rule: LimitSchemaValue;
}
interface ExpandedSchema extends UnknownExpandedRangeSchema {
rule: LimitSchemaValue;
}
type Schema = ExpandedSchema | LimitSchemaValue;
interface ErrorContext extends BaseErrorContext<"after">, Inner {
}
interface Declaration extends declareNode<{
kind: "after";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: Date;
errorContext: ErrorContext;
}> {
}
type Node = AfterNode;
}
export declare class AfterNode extends BaseRange<After.Declaration> {
impliedBasis: BaseRoot;
collapsibleLimitString: string;
traverseAllows: TraverseAllows<Date>;
reduceJsonSchema(base: JsonSchema, ctx: ToJsonSchema.Context): JsonSchema;
}
export declare const After: {
implementation: nodeImplementationOf<After.Declaration>;
Node: typeof AfterNode;
};

View File

@@ -0,0 +1,35 @@
import { describeCollapsibleDate } from "@ark/util";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { BaseRange, createDateSchemaNormalizer, parseDateLimit } from "./range.js";
const implementation = implementNode({
kind: "after",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: {
parse: parseDateLimit,
serialize: schema => schema.toISOString()
}
},
normalize: createDateSchemaNormalizer("after"),
defaults: {
description: node => `${node.collapsibleLimitString} or later`,
actual: describeCollapsibleDate
},
intersections: {
after: (l, r) => (l.isStricterThan(r) ? l : r)
}
});
export class AfterNode extends BaseRange {
impliedBasis = $ark.intrinsic.Date.internal;
collapsibleLimitString = describeCollapsibleDate(this.rule);
traverseAllows = data => data >= this.rule;
reduceJsonSchema(base, ctx) {
return ctx.fallback.date({ code: "date", base, after: this.rule });
}
}
export const After = {
implementation,
Node: AfterNode
};

View File

@@ -0,0 +1,41 @@
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { ToJsonSchema } from "../shared/toJsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { BaseRange, type BaseRangeInner, type LimitSchemaValue, type UnknownExpandedRangeSchema, type UnknownNormalizedRangeSchema } from "./range.ts";
export declare namespace Before {
interface Inner extends BaseRangeInner {
rule: Date;
}
interface NormalizedSchema extends UnknownNormalizedRangeSchema {
rule: LimitSchemaValue;
}
interface ExpandedSchema extends UnknownExpandedRangeSchema {
rule: LimitSchemaValue;
}
type Schema = ExpandedSchema | LimitSchemaValue;
interface ErrorContext extends BaseErrorContext<"before">, Inner {
}
interface Declaration extends declareNode<{
kind: "before";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: Date;
errorContext: ErrorContext;
}> {
}
type Node = BeforeNode;
}
export declare class BeforeNode extends BaseRange<Before.Declaration> {
collapsibleLimitString: string;
traverseAllows: TraverseAllows<Date>;
impliedBasis: BaseRoot;
reduceJsonSchema(base: JsonSchema, ctx: ToJsonSchema.Context): JsonSchema;
}
export declare const Before: {
implementation: nodeImplementationOf<Before.Declaration>;
Node: typeof BeforeNode;
};

View File

@@ -0,0 +1,41 @@
import { describeCollapsibleDate } from "@ark/util";
import { Disjoint } from "../shared/disjoint.js";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { BaseRange, createDateSchemaNormalizer, parseDateLimit } from "./range.js";
const implementation = implementNode({
kind: "before",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: {
parse: parseDateLimit,
serialize: schema => schema.toISOString()
}
},
normalize: createDateSchemaNormalizer("before"),
defaults: {
description: node => `${node.collapsibleLimitString} or earlier`,
actual: describeCollapsibleDate
},
intersections: {
before: (l, r) => (l.isStricterThan(r) ? l : r),
after: (before, after, ctx) => before.overlapsRange(after) ?
before.overlapIsUnit(after) ?
ctx.$.node("unit", { unit: before.rule })
: null
: Disjoint.init("range", before, after)
}
});
export class BeforeNode extends BaseRange {
collapsibleLimitString = describeCollapsibleDate(this.rule);
traverseAllows = data => data <= this.rule;
impliedBasis = $ark.intrinsic.Date.internal;
reduceJsonSchema(base, ctx) {
return ctx.fallback.date({ code: "date", base, before: this.rule });
}
}
export const Before = {
implementation,
Node: BeforeNode
};

View File

@@ -0,0 +1,43 @@
import { InternalPrimitiveConstraint, writeInvalidOperandMessage } from "../constraint.ts";
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, BaseNormalizedSchema, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
export declare namespace Divisor {
interface Inner {
readonly rule: number;
}
interface NormalizedSchema extends BaseNormalizedSchema {
readonly rule: number;
}
type Schema = NormalizedSchema | number;
interface ErrorContext extends BaseErrorContext<"divisor">, Inner {
}
interface Declaration extends declareNode<{
kind: "divisor";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: number;
errorContext: ErrorContext;
}> {
}
type Node = DivisorNode;
}
export declare class DivisorNode extends InternalPrimitiveConstraint<Divisor.Declaration> {
traverseAllows: TraverseAllows<number>;
readonly compiledCondition: string;
readonly compiledNegation: string;
readonly impliedBasis: BaseRoot;
readonly expression: string;
reduceJsonSchema(schema: JsonSchema.Numeric): JsonSchema.Numeric;
}
export declare const Divisor: {
implementation: nodeImplementationOf<Divisor.Declaration>;
Node: typeof DivisorNode;
};
export declare const writeIndivisibleMessage: (t: BaseRoot) => string;
export type writeIndivisibleMessage<actual> = writeInvalidOperandMessage<"divisor", actual>;
export declare const writeNonIntegerDivisorMessage: <divisor extends number>(divisor: divisor) => writeNonIntegerDivisorMessage<divisor>;
export type writeNonIntegerDivisorMessage<divisor extends number> = `divisor must be an integer (was ${divisor})`;

View File

@@ -0,0 +1,58 @@
import { throwParseError } from "@ark/util";
import { InternalPrimitiveConstraint, writeInvalidOperandMessage } from "../constraint.js";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
const implementation = implementNode({
kind: "divisor",
collapsibleKey: "rule",
keys: {
rule: {
parse: divisor => Number.isInteger(divisor) ? divisor : (throwParseError(writeNonIntegerDivisorMessage(divisor)))
}
},
normalize: schema => typeof schema === "number" ? { rule: schema } : schema,
hasAssociatedError: true,
defaults: {
description: node => node.rule === 1 ? "an integer"
: node.rule === 2 ? "even"
: `a multiple of ${node.rule}`
},
intersections: {
divisor: (l, r, ctx) => ctx.$.node("divisor", {
rule: Math.abs((l.rule * r.rule) / greatestCommonDivisor(l.rule, r.rule))
})
},
obviatesBasisDescription: true
});
export class DivisorNode extends InternalPrimitiveConstraint {
traverseAllows = data => data % this.rule === 0;
compiledCondition = `data % ${this.rule} === 0`;
compiledNegation = `data % ${this.rule} !== 0`;
impliedBasis = $ark.intrinsic.number.internal;
expression = `% ${this.rule}`;
reduceJsonSchema(schema) {
schema.type = "integer";
if (this.rule === 1)
return schema;
schema.multipleOf = this.rule;
return schema;
}
}
export const Divisor = {
implementation,
Node: DivisorNode
};
export const writeIndivisibleMessage = (t) => writeInvalidOperandMessage("divisor", $ark.intrinsic.number, t);
export const writeNonIntegerDivisorMessage = (divisor) => `divisor must be an integer (was ${divisor})`;
// https://en.wikipedia.org/wiki/Euclidean_algorithm
const greatestCommonDivisor = (l, r) => {
let previous;
let greatestCommonDivisor = l;
let current = r;
while (current !== 0) {
previous = current;
current = greatestCommonDivisor % current;
greatestCommonDivisor = previous;
}
return greatestCommonDivisor;
};

View File

@@ -0,0 +1,49 @@
import { InternalPrimitiveConstraint } from "../constraint.ts";
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, BaseNormalizedSchema, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { type LengthBoundableData } from "./range.ts";
export declare namespace ExactLength {
interface Inner {
readonly rule: number;
}
interface NormalizedSchema extends BaseNormalizedSchema {
readonly rule: number;
}
type Schema = NormalizedSchema | number;
interface ErrorContext extends BaseErrorContext<"exactLength">, Inner {
}
type Declaration = declareNode<{
kind: "exactLength";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: LengthBoundableData;
errorContext: ErrorContext;
}>;
type Node = ExactLengthNode;
}
export declare class ExactLengthNode extends InternalPrimitiveConstraint<ExactLength.Declaration> {
traverseAllows: TraverseAllows<LengthBoundableData>;
readonly compiledCondition: string;
readonly compiledNegation: string;
readonly impliedBasis: BaseRoot;
readonly expression: string;
reduceJsonSchema(schema: JsonSchema.LengthBoundable): JsonSchema.LengthBoundable;
}
export declare const ExactLength: {
implementation: nodeImplementationOf<{
intersectionIsOpen: false;
childKind: never;
reducibleTo: "exactLength";
kind: "exactLength";
schema: ExactLength.Schema;
normalizedSchema: ExactLength.NormalizedSchema;
inner: ExactLength.Inner;
prerequisite: LengthBoundableData;
errorContext: ExactLength.ErrorContext;
}>;
Node: typeof ExactLengthNode;
};

View File

@@ -0,0 +1,55 @@
import { InternalPrimitiveConstraint } from "../constraint.js";
import { Disjoint } from "../shared/disjoint.js";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { ToJsonSchema } from "../shared/toJsonSchema.js";
import { createLengthRuleParser } from "./range.js";
const implementation = implementNode({
kind: "exactLength",
collapsibleKey: "rule",
keys: {
rule: {
parse: createLengthRuleParser("exactLength")
}
},
normalize: schema => typeof schema === "number" ? { rule: schema } : schema,
hasAssociatedError: true,
defaults: {
description: node => `exactly length ${node.rule}`,
actual: data => `${data.length}`
},
intersections: {
exactLength: (l, r, ctx) => Disjoint.init("unit", ctx.$.node("unit", { unit: l.rule }), ctx.$.node("unit", { unit: r.rule }), { path: ["length"] }),
minLength: (exactLength, minLength) => exactLength.rule >= minLength.rule ?
exactLength
: Disjoint.init("range", exactLength, minLength),
maxLength: (exactLength, maxLength) => exactLength.rule <= maxLength.rule ?
exactLength
: Disjoint.init("range", exactLength, maxLength)
}
});
export class ExactLengthNode extends InternalPrimitiveConstraint {
traverseAllows = data => data.length === this.rule;
compiledCondition = `data.length === ${this.rule}`;
compiledNegation = `data.length !== ${this.rule}`;
impliedBasis = $ark.intrinsic.lengthBoundable.internal;
expression = `== ${this.rule}`;
reduceJsonSchema(schema) {
switch (schema.type) {
case "string":
schema.minLength = this.rule;
schema.maxLength = this.rule;
return schema;
case "array":
schema.minItems = this.rule;
schema.maxItems = this.rule;
return schema;
default:
return ToJsonSchema.throwInternalOperandError("exactLength", schema);
}
}
}
export const ExactLength = {
implementation,
Node: ExactLengthNode
};

View File

@@ -0,0 +1,34 @@
import type { BaseConstraint } from "../constraint.ts";
import type { nodeImplementationOf } from "../shared/implement.ts";
import { After } from "./after.ts";
import { Before } from "./before.ts";
import { ExactLength } from "./exactLength.ts";
import { Max } from "./max.ts";
import { MaxLength } from "./maxLength.ts";
import { Min } from "./min.ts";
import { MinLength } from "./minLength.ts";
export interface BoundDeclarations {
min: Min.Declaration;
max: Max.Declaration;
minLength: MinLength.Declaration;
maxLength: MaxLength.Declaration;
exactLength: ExactLength.Declaration;
after: After.Declaration;
before: Before.Declaration;
}
export interface BoundNodesByKind {
min: Min.Node;
max: Max.Node;
minLength: MinLength.Node;
maxLength: MaxLength.Node;
exactLength: ExactLength.Node;
after: After.Node;
before: Before.Node;
}
export type BoundKind = keyof BoundDeclarations;
export type RangeKind = Exclude<BoundKind, "exactLength">;
export type boundImplementationsByKind = {
[k in BoundKind]: nodeImplementationOf<BoundDeclarations[k]>;
};
export declare const boundImplementationsByKind: boundImplementationsByKind;
export declare const boundClassesByKind: Record<BoundKind, typeof BaseConstraint<any>>;

View File

@@ -0,0 +1,25 @@
import { After } from "./after.js";
import { Before } from "./before.js";
import { ExactLength } from "./exactLength.js";
import { Max } from "./max.js";
import { MaxLength } from "./maxLength.js";
import { Min } from "./min.js";
import { MinLength } from "./minLength.js";
export const boundImplementationsByKind = {
min: Min.implementation,
max: Max.implementation,
minLength: MinLength.implementation,
maxLength: MaxLength.implementation,
exactLength: ExactLength.implementation,
after: After.implementation,
before: Before.implementation
};
export const boundClassesByKind = {
min: Min.Node,
max: Max.Node,
minLength: MinLength.Node,
maxLength: MaxLength.Node,
exactLength: ExactLength.Node,
after: After.Node,
before: Before.Node
};

View File

@@ -0,0 +1,37 @@
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { BaseRange, type BaseRangeInner, type UnknownExpandedRangeSchema } from "./range.ts";
export declare namespace Max {
interface Inner extends BaseRangeInner {
rule: number;
exclusive?: true;
}
interface NormalizedSchema extends UnknownExpandedRangeSchema {
rule: number;
}
type Schema = NormalizedSchema | number;
interface ErrorContext extends BaseErrorContext<"max">, Inner {
}
interface Declaration extends declareNode<{
kind: "max";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: number;
errorContext: ErrorContext;
}> {
}
type Node = MaxNode;
}
export declare class MaxNode extends BaseRange<Max.Declaration> {
impliedBasis: BaseRoot;
traverseAllows: TraverseAllows<number>;
reduceJsonSchema(schema: JsonSchema.Numeric): JsonSchema.Numeric;
}
export declare const Max: {
implementation: nodeImplementationOf<Max.Declaration>;
Node: typeof MaxNode;
};

View File

@@ -0,0 +1,45 @@
import { Disjoint } from "../shared/disjoint.js";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { BaseRange, parseExclusiveKey } from "./range.js";
const implementation = implementNode({
kind: "max",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: {},
exclusive: parseExclusiveKey
},
normalize: schema => typeof schema === "number" ? { rule: schema } : schema,
defaults: {
description: node => {
if (node.rule === 0)
return node.exclusive ? "negative" : "non-positive";
return `${node.exclusive ? "less than" : "at most"} ${node.rule}`;
}
},
intersections: {
max: (l, r) => (l.isStricterThan(r) ? l : r),
min: (max, min, ctx) => max.overlapsRange(min) ?
max.overlapIsUnit(min) ?
ctx.$.node("unit", { unit: max.rule })
: null
: Disjoint.init("range", max, min)
},
obviatesBasisDescription: true
});
export class MaxNode extends BaseRange {
impliedBasis = $ark.intrinsic.number.internal;
traverseAllows = this.exclusive ? data => data < this.rule : data => data <= this.rule;
reduceJsonSchema(schema) {
if (this.exclusive)
schema.exclusiveMaximum = this.rule;
else
schema.maximum = this.rule;
return schema;
}
}
export const Max = {
implementation,
Node: MaxNode
};

View File

@@ -0,0 +1,40 @@
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { BaseRange, type BaseRangeInner, type LengthBoundableData, type UnknownExpandedRangeSchema, type UnknownNormalizedRangeSchema } from "./range.ts";
export declare namespace MaxLength {
interface Inner extends BaseRangeInner {
rule: number;
}
interface NormalizedSchema extends UnknownNormalizedRangeSchema {
rule: number;
}
interface ExpandedSchema extends UnknownExpandedRangeSchema {
rule: number;
}
type Schema = ExpandedSchema | number;
interface ErrorContext extends BaseErrorContext<"maxLength">, Inner {
}
interface Declaration extends declareNode<{
kind: "maxLength";
schema: Schema;
reducibleTo: "exactLength";
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: LengthBoundableData;
errorContext: ErrorContext;
}> {
}
type Node = MaxLengthNode;
}
export declare class MaxLengthNode extends BaseRange<MaxLength.Declaration> {
readonly impliedBasis: BaseRoot;
traverseAllows: TraverseAllows<LengthBoundableData>;
reduceJsonSchema(schema: JsonSchema.LengthBoundable): JsonSchema.LengthBoundable;
}
export declare const MaxLength: {
implementation: nodeImplementationOf<MaxLength.Declaration>;
Node: typeof MaxLengthNode;
};

View File

@@ -0,0 +1,49 @@
import { Disjoint } from "../shared/disjoint.js";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { ToJsonSchema } from "../shared/toJsonSchema.js";
import { BaseRange, createLengthRuleParser, createLengthSchemaNormalizer } from "./range.js";
const implementation = implementNode({
kind: "maxLength",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: {
parse: createLengthRuleParser("maxLength")
}
},
reduce: (inner, $) => inner.rule === 0 ? $.node("exactLength", inner) : undefined,
normalize: createLengthSchemaNormalizer("maxLength"),
defaults: {
description: node => `at most length ${node.rule}`,
actual: data => `${data.length}`
},
intersections: {
maxLength: (l, r) => (l.isStricterThan(r) ? l : r),
minLength: (max, min, ctx) => max.overlapsRange(min) ?
max.overlapIsUnit(min) ?
ctx.$.node("exactLength", { rule: max.rule })
: null
: Disjoint.init("range", max, min)
}
});
export class MaxLengthNode extends BaseRange {
impliedBasis = $ark.intrinsic.lengthBoundable.internal;
traverseAllows = data => data.length <= this.rule;
reduceJsonSchema(schema) {
switch (schema.type) {
case "string":
schema.maxLength = this.rule;
return schema;
case "array":
schema.maxItems = this.rule;
return schema;
default:
return ToJsonSchema.throwInternalOperandError("maxLength", schema);
}
}
}
export const MaxLength = {
implementation,
Node: MaxLengthNode
};

View File

@@ -0,0 +1,37 @@
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { BaseRange, type BaseRangeInner, type UnknownExpandedRangeSchema } from "./range.ts";
export declare namespace Min {
interface Inner extends BaseRangeInner {
rule: number;
exclusive?: true;
}
interface NormalizedSchema extends UnknownExpandedRangeSchema {
rule: number;
}
type Schema = NormalizedSchema | number;
interface ErrorContext extends BaseErrorContext<"min">, Inner {
}
interface Declaration extends declareNode<{
kind: "min";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: number;
errorContext: ErrorContext;
}> {
}
type Node = MinNode;
}
export declare class MinNode extends BaseRange<Min.Declaration> {
readonly impliedBasis: BaseRoot;
traverseAllows: TraverseAllows<number>;
reduceJsonSchema(schema: JsonSchema.Numeric): JsonSchema.Numeric;
}
export declare const Min: {
implementation: nodeImplementationOf<Min.Declaration>;
Node: typeof MinNode;
};

View File

@@ -0,0 +1,39 @@
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { BaseRange, parseExclusiveKey } from "./range.js";
const implementation = implementNode({
kind: "min",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: {},
exclusive: parseExclusiveKey
},
normalize: schema => typeof schema === "number" ? { rule: schema } : schema,
defaults: {
description: node => {
if (node.rule === 0)
return node.exclusive ? "positive" : "non-negative";
return `${node.exclusive ? "more than" : "at least"} ${node.rule}`;
}
},
intersections: {
min: (l, r) => (l.isStricterThan(r) ? l : r)
},
obviatesBasisDescription: true
});
export class MinNode extends BaseRange {
impliedBasis = $ark.intrinsic.number.internal;
traverseAllows = this.exclusive ? data => data > this.rule : data => data >= this.rule;
reduceJsonSchema(schema) {
if (this.exclusive)
schema.exclusiveMinimum = this.rule;
else
schema.minimum = this.rule;
return schema;
}
}
export const Min = {
implementation,
Node: MinNode
};

View File

@@ -0,0 +1,40 @@
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { TraverseAllows } from "../shared/traversal.ts";
import { BaseRange, type BaseRangeInner, type LengthBoundableData, type UnknownExpandedRangeSchema, type UnknownNormalizedRangeSchema } from "./range.ts";
export declare namespace MinLength {
interface Inner extends BaseRangeInner {
rule: number;
}
interface NormalizedSchema extends UnknownNormalizedRangeSchema {
rule: number;
}
interface ExpandedSchema extends UnknownExpandedRangeSchema {
rule: number;
}
type Schema = ExpandedSchema | number;
interface ErrorContext extends BaseErrorContext<"minLength">, Inner {
}
interface Declaration extends declareNode<{
kind: "minLength";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
prerequisite: LengthBoundableData;
reducibleTo: "intersection";
errorContext: ErrorContext;
}> {
}
type Node = MinLengthNode;
}
export declare class MinLengthNode extends BaseRange<MinLength.Declaration> {
readonly impliedBasis: BaseRoot;
traverseAllows: TraverseAllows<LengthBoundableData>;
reduceJsonSchema(schema: JsonSchema.LengthBoundable): JsonSchema.LengthBoundable;
}
export declare const MinLength: {
implementation: nodeImplementationOf<MinLength.Declaration>;
Node: typeof MinLengthNode;
};

View File

@@ -0,0 +1,47 @@
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
import { ToJsonSchema } from "../shared/toJsonSchema.js";
import { BaseRange, createLengthRuleParser, createLengthSchemaNormalizer } from "./range.js";
const implementation = implementNode({
kind: "minLength",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: {
parse: createLengthRuleParser("minLength")
}
},
reduce: inner => inner.rule === 0 ?
// a minimum length of zero is trivially satisfied
$ark.intrinsic.unknown
: undefined,
normalize: createLengthSchemaNormalizer("minLength"),
defaults: {
description: node => node.rule === 1 ? "non-empty" : `at least length ${node.rule}`,
// avoid default message like "must be non-empty (was 0)"
actual: data => (data.length === 0 ? "" : `${data.length}`)
},
intersections: {
minLength: (l, r) => (l.isStricterThan(r) ? l : r)
}
});
export class MinLengthNode extends BaseRange {
impliedBasis = $ark.intrinsic.lengthBoundable.internal;
traverseAllows = data => data.length >= this.rule;
reduceJsonSchema(schema) {
switch (schema.type) {
case "string":
schema.minLength = this.rule;
return schema;
case "array":
schema.minItems = this.rule;
return schema;
default:
return ToJsonSchema.throwInternalOperandError("minLength", schema);
}
}
}
export const MinLength = {
implementation,
Node: MinLengthNode
};

View File

@@ -0,0 +1,43 @@
import { InternalPrimitiveConstraint } from "../constraint.ts";
import type { BaseRoot } from "../roots/root.ts";
import type { BaseErrorContext, BaseNormalizedSchema, declareNode } from "../shared/declare.ts";
import { type nodeImplementationOf } from "../shared/implement.ts";
import type { JsonSchema } from "../shared/jsonSchema.ts";
import type { ToJsonSchema } from "../shared/toJsonSchema.ts";
export declare namespace Pattern {
interface NormalizedSchema extends BaseNormalizedSchema {
readonly rule: string;
readonly flags?: string;
}
interface Inner {
readonly rule: string;
readonly flags?: string;
}
type Schema = NormalizedSchema | string | RegExp;
interface ErrorContext extends BaseErrorContext<"pattern">, Inner {
}
interface Declaration extends declareNode<{
kind: "pattern";
schema: Schema;
normalizedSchema: NormalizedSchema;
inner: Inner;
intersectionIsOpen: true;
prerequisite: string;
errorContext: ErrorContext;
}> {
}
type Node = PatternNode;
}
export declare class PatternNode extends InternalPrimitiveConstraint<Pattern.Declaration> {
readonly instance: RegExp;
readonly expression: string;
traverseAllows: (string: string) => boolean;
readonly compiledCondition: string;
readonly compiledNegation: string;
readonly impliedBasis: BaseRoot;
reduceJsonSchema(base: JsonSchema.String, ctx: ToJsonSchema.Context): JsonSchema.String;
}
export declare const Pattern: {
implementation: nodeImplementationOf<Pattern.Declaration>;
Node: typeof PatternNode;
};

View File

@@ -0,0 +1,52 @@
import { InternalPrimitiveConstraint } from "../constraint.js";
import { implementNode } from "../shared/implement.js";
import { $ark } from "../shared/registry.js";
const implementation = implementNode({
kind: "pattern",
collapsibleKey: "rule",
keys: {
rule: {},
flags: {}
},
normalize: schema => typeof schema === "string" ? { rule: schema }
: schema instanceof RegExp ?
schema.flags ?
{ rule: schema.source, flags: schema.flags }
: { rule: schema.source }
: schema,
obviatesBasisDescription: true,
obviatesBasisExpression: true,
hasAssociatedError: true,
intersectionIsOpen: true,
defaults: {
description: node => `matched by ${node.rule}`
},
intersections: {
// for now, non-equal regex are naively intersected:
// https://github.com/arktypeio/arktype/issues/853
pattern: () => null
}
});
export class PatternNode extends InternalPrimitiveConstraint {
instance = new RegExp(this.rule, this.flags);
expression = `${this.instance}`;
traverseAllows = this.instance.test.bind(this.instance);
compiledCondition = `${this.expression}.test(data)`;
compiledNegation = `!${this.compiledCondition}`;
impliedBasis = $ark.intrinsic.string.internal;
reduceJsonSchema(base, ctx) {
if (base.pattern) {
return ctx.fallback.patternIntersection({
code: "patternIntersection",
base: base,
pattern: this.rule
});
}
base.pattern = this.rule;
return base;
}
}
export const Pattern = {
implementation,
Node: PatternNode
};

View File

@@ -0,0 +1,106 @@
import { type array, type propValueOf, type satisfy } from "@ark/util";
import { InternalPrimitiveConstraint } from "../constraint.ts";
import type { Declaration, nodeOfKind, NodeSchema, NormalizedSchema } from "../kinds.ts";
import type { BaseNodeDeclaration, BaseNormalizedSchema } from "../shared/declare.ts";
import type { keySchemaDefinitions } from "../shared/implement.ts";
import type { RangeKind } from "./kinds.ts";
export interface BaseRangeDeclaration extends BaseNodeDeclaration {
kind: RangeKind;
inner: BaseRangeInner;
normalizedSchema: UnknownExpandedRangeSchema;
}
export declare abstract class BaseRange<d extends BaseRangeDeclaration> extends InternalPrimitiveConstraint<d> {
readonly exclusive?: true;
readonly boundOperandKind: OperandKindsByBoundKind[d["kind"]];
readonly compiledActual: string;
readonly comparator: RelativeComparator;
readonly numericLimit: number;
readonly expression: string;
readonly compiledCondition: string;
readonly compiledNegation: string;
readonly stringLimit: string;
readonly limitKind: LimitKind;
isStricterThan(r: nodeOfKind<d["kind"] | pairedRangeKind<d["kind"]>>): boolean;
overlapsRange(r: nodeOfKind<pairedRangeKind<d["kind"]>>): boolean;
overlapIsUnit(r: nodeOfKind<pairedRangeKind<d["kind"]>>): boolean;
}
export interface BaseRangeInner {
readonly rule: LimitValue;
}
export type LimitValue = Date | number;
export type LimitSchemaValue = Date | number | string;
export type LimitInnerValue<kind extends RangeKind = RangeKind> = kind extends "before" | "after" ? Date : number;
export interface UnknownExpandedRangeSchema extends BaseNormalizedSchema {
readonly rule: LimitSchemaValue;
readonly exclusive?: boolean;
}
export interface UnknownNormalizedRangeSchema extends BaseNormalizedSchema {
readonly rule: LimitSchemaValue;
}
export type UnknownRangeSchema = LimitSchemaValue | UnknownExpandedRangeSchema;
export interface ExclusiveExpandedDateRangeSchema extends BaseNormalizedSchema {
rule: LimitSchemaValue;
exclusive?: true;
}
export type ExclusiveDateRangeSchema = LimitSchemaValue | ExclusiveExpandedDateRangeSchema;
export interface InclusiveExpandedDateRangeSchema extends BaseNormalizedSchema {
rule: LimitSchemaValue;
exclusive?: false;
}
export type InclusiveDateRangeSchema = LimitSchemaValue | InclusiveExpandedDateRangeSchema;
export interface ExclusiveNormalizedNumericRangeSchema extends BaseNormalizedSchema {
rule: number;
exclusive?: true;
}
export type ExclusiveNumericRangeSchema = number | ExclusiveNormalizedNumericRangeSchema;
export interface InclusiveNormalizedNumericRangeSchema extends BaseNormalizedSchema {
rule: number;
exclusive?: false;
}
export type InclusiveNumericRangeSchema = number | InclusiveNormalizedNumericRangeSchema;
export type LimitKind = "lower" | "upper";
export type RelativeComparator<kind extends LimitKind = LimitKind> = {
lower: ">" | ">=";
upper: "<" | "<=";
}[kind];
export declare const boundKindPairsByLower: BoundKindPairsByLower;
type BoundKindPairsByLower = {
min: "max";
minLength: "maxLength";
after: "before";
};
type BoundKindPairsByUpper = {
max: "min";
maxLength: "minLength";
before: "after";
};
export type pairedRangeKind<kind extends RangeKind> = kind extends LowerBoundKind ? BoundKindPairsByLower[kind] : BoundKindPairsByUpper[kind & UpperBoundKind];
export type LowerBoundKind = keyof typeof boundKindPairsByLower;
export type LowerNode = nodeOfKind<LowerBoundKind>;
export type UpperBoundKind = propValueOf<typeof boundKindPairsByLower>;
export type UpperNode = nodeOfKind<UpperBoundKind>;
export type NumericallyBoundable = string | number | array;
export type Boundable = NumericallyBoundable | Date;
export declare const parseExclusiveKey: keySchemaDefinitions<Declaration<"min" | "max">>["exclusive"];
export declare const createLengthSchemaNormalizer: <kind extends "minLength" | "maxLength">(kind: kind) => (schema: NodeSchema<kind>) => NormalizedSchema<kind>;
export declare const createDateSchemaNormalizer: <kind extends DateRangeKind>(kind: kind) => (schema: NodeSchema<kind>) => NormalizedSchema<kind>;
export declare const parseDateLimit: (limit: LimitSchemaValue) => Date;
export type LengthBoundKind = "minLength" | "maxLength" | "exactLength";
export declare const writeInvalidLengthBoundMessage: (kind: LengthBoundKind, limit: number) => string;
export declare const createLengthRuleParser: (kind: LengthBoundKind) => (limit: number) => number | undefined;
type OperandKindsByBoundKind = satisfy<Record<RangeKind, BoundOperandKind>, {
min: "value";
max: "value";
minLength: "length";
maxLength: "length";
after: "date";
before: "date";
}>;
export declare const compileComparator: (kind: RangeKind, exclusive: boolean | undefined) => RelativeComparator;
export type BoundOperandKind = "value" | "length" | "date";
export type LengthBoundableData = string | array;
export type DateRangeKind = "before" | "after";
export declare const dateLimitToString: (limit: LimitSchemaValue) => string;
export declare const writeUnboundableMessage: <root extends string>(root: root) => writeUnboundableMessage<root>;
export type writeUnboundableMessage<root extends string> = `Bounded expression ${root} must be exactly one of number, string, Array, or Date`;
export {};

View File

@@ -0,0 +1,103 @@
import { isKeyOf, throwParseError } from "@ark/util";
import { InternalPrimitiveConstraint } from "../constraint.js";
export class BaseRange extends InternalPrimitiveConstraint {
boundOperandKind = operandKindsByBoundKind[this.kind];
compiledActual = this.boundOperandKind === "value" ? `data`
: this.boundOperandKind === "length" ? `data.length`
: `data.valueOf()`;
comparator = compileComparator(this.kind, this.exclusive);
numericLimit = this.rule.valueOf();
expression = `${this.comparator} ${this.rule}`;
compiledCondition = `${this.compiledActual} ${this.comparator} ${this.numericLimit}`;
compiledNegation = `${this.compiledActual} ${negatedComparators[this.comparator]} ${this.numericLimit}`;
// we need to compute stringLimit before errorContext, which references it
// transitively through description for date bounds
stringLimit = this.boundOperandKind === "date" ?
dateLimitToString(this.numericLimit)
: `${this.numericLimit}`;
limitKind = this.comparator["0"] === "<" ? "upper" : "lower";
isStricterThan(r) {
const thisLimitIsStricter = this.limitKind === "upper" ?
this.numericLimit < r.numericLimit
: this.numericLimit > r.numericLimit;
return (thisLimitIsStricter ||
(this.numericLimit === r.numericLimit &&
this.exclusive === true &&
!r.exclusive));
}
overlapsRange(r) {
if (this.isStricterThan(r))
return false;
if (this.numericLimit === r.numericLimit && (this.exclusive || r.exclusive))
return false;
return true;
}
overlapIsUnit(r) {
return (this.numericLimit === r.numericLimit && !this.exclusive && !r.exclusive);
}
}
const negatedComparators = {
"<": ">=",
"<=": ">",
">": "<=",
">=": "<"
};
export const boundKindPairsByLower = {
min: "max",
minLength: "maxLength",
after: "before"
};
export const parseExclusiveKey = {
// omit key with value false since it is the default
parse: (flag) => flag || undefined
};
export const createLengthSchemaNormalizer = (kind) => (schema) => {
if (typeof schema === "number")
return { rule: schema };
const { exclusive, ...normalized } = schema;
return exclusive ?
{
...normalized,
rule: kind === "minLength" ? normalized.rule + 1 : normalized.rule - 1
}
: normalized;
};
export const createDateSchemaNormalizer = (kind) => (schema) => {
if (typeof schema === "number" ||
typeof schema === "string" ||
schema instanceof Date)
return { rule: schema };
const { exclusive, ...normalized } = schema;
if (!exclusive)
return normalized;
const numericLimit = typeof normalized.rule === "number" ? normalized.rule
: typeof normalized.rule === "string" ?
new Date(normalized.rule).valueOf()
: normalized.rule.valueOf();
return exclusive ?
{
...normalized,
rule: kind === "after" ? numericLimit + 1 : numericLimit - 1
}
: normalized;
};
export const parseDateLimit = (limit) => typeof limit === "string" || typeof limit === "number" ?
new Date(limit)
: limit;
export const writeInvalidLengthBoundMessage = (kind, limit) => `${kind} bound must be a positive integer (was ${limit})`;
export const createLengthRuleParser = (kind) => (limit) => {
if (!Number.isInteger(limit) || limit < 0)
throwParseError(writeInvalidLengthBoundMessage(kind, limit));
return limit;
};
const operandKindsByBoundKind = {
min: "value",
max: "value",
minLength: "length",
maxLength: "length",
after: "date",
before: "date"
};
export const compileComparator = (kind, exclusive) => `${isKeyOf(kind, boundKindPairsByLower) ? ">" : "<"}${exclusive ? "" : "="}`;
export const dateLimitToString = (limit) => typeof limit === "string" ? limit : new Date(limit).toLocaleString();
export const writeUnboundableMessage = (root) => `Bounded expression ${root} must be exactly one of number, string, Array, or Date`;