- 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
216 lines
7.9 KiB
JavaScript
216 lines
7.9 KiB
JavaScript
import { domainDescriptions, entriesOf, flatMorph, hasDomain, isArray, isEmptyObject, printable, throwInternalError, throwParseError, unset } from "@ark/util";
|
|
import { nodeClassesByKind, nodeImplementationsByKind } from "./kinds.js";
|
|
import { Disjoint } from "./shared/disjoint.js";
|
|
import { constraintKeys, defaultValueSerializer, isNodeKind, precedenceOfKind } from "./shared/implement.js";
|
|
import { $ark } from "./shared/registry.js";
|
|
import { hasArkKind, isNode } from "./shared/utils.js";
|
|
export const schemaKindOf = (schema, allowedKinds) => {
|
|
const kind = discriminateRootKind(schema);
|
|
if (allowedKinds && !allowedKinds.includes(kind)) {
|
|
return throwParseError(`Root of kind ${kind} should be one of ${allowedKinds}`);
|
|
}
|
|
return kind;
|
|
};
|
|
const discriminateRootKind = (schema) => {
|
|
if (hasArkKind(schema, "root"))
|
|
return schema.kind;
|
|
if (typeof schema === "string") {
|
|
return (schema[0] === "$" ? "alias"
|
|
: schema in domainDescriptions ? "domain"
|
|
: "proto");
|
|
}
|
|
if (typeof schema === "function")
|
|
return "proto";
|
|
// throw at end of function
|
|
if (typeof schema !== "object" || schema === null)
|
|
return throwParseError(writeInvalidSchemaMessage(schema));
|
|
if ("morphs" in schema)
|
|
return "morph";
|
|
if ("branches" in schema || isArray(schema))
|
|
return "union";
|
|
if ("unit" in schema)
|
|
return "unit";
|
|
if ("reference" in schema)
|
|
return "alias";
|
|
const schemaKeys = Object.keys(schema);
|
|
if (schemaKeys.length === 0 || schemaKeys.some(k => k in constraintKeys))
|
|
return "intersection";
|
|
if ("proto" in schema)
|
|
return "proto";
|
|
if ("domain" in schema)
|
|
return "domain";
|
|
return throwParseError(writeInvalidSchemaMessage(schema));
|
|
};
|
|
export const writeInvalidSchemaMessage = (schema) => `${printable(schema)} is not a valid type schema`;
|
|
const nodeCountsByPrefix = {};
|
|
const serializeListableChild = (listableNode) => isArray(listableNode) ?
|
|
listableNode.map(node => node.collapsibleJson)
|
|
: listableNode.collapsibleJson;
|
|
export const nodesByRegisteredId = {};
|
|
$ark.nodesByRegisteredId = nodesByRegisteredId;
|
|
export const registerNodeId = (prefix) => {
|
|
nodeCountsByPrefix[prefix] ??= 0;
|
|
return `${prefix}${++nodeCountsByPrefix[prefix]}`;
|
|
};
|
|
export const parseNode = (ctx) => {
|
|
const impl = nodeImplementationsByKind[ctx.kind];
|
|
const configuredSchema = impl.applyConfig?.(ctx.def, ctx.$.resolvedConfig) ?? ctx.def;
|
|
const inner = {};
|
|
const { meta: metaSchema, ...innerSchema } = configuredSchema;
|
|
const meta = metaSchema === undefined ? {}
|
|
: typeof metaSchema === "string" ? { description: metaSchema }
|
|
: metaSchema;
|
|
// ensure node entries are parsed in order of precedence, with non-children
|
|
// parsed first
|
|
const innerSchemaEntries = entriesOf(innerSchema)
|
|
.sort(([lKey], [rKey]) => isNodeKind(lKey) ?
|
|
isNodeKind(rKey) ? precedenceOfKind(lKey) - precedenceOfKind(rKey)
|
|
: 1
|
|
: isNodeKind(rKey) ? -1
|
|
: lKey < rKey ? -1
|
|
: 1)
|
|
.filter(([k, v]) => {
|
|
// move meta. prefixed props to meta, overwriting existing nested
|
|
// props of the same name if they exist
|
|
if (k.startsWith("meta.")) {
|
|
const metaKey = k.slice(5);
|
|
meta[metaKey] = v;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
for (const entry of innerSchemaEntries) {
|
|
const k = entry[0];
|
|
const keyImpl = impl.keys[k];
|
|
if (!keyImpl)
|
|
return throwParseError(`Key ${k} is not valid on ${ctx.kind} schema`);
|
|
const v = keyImpl.parse ? keyImpl.parse(entry[1], ctx) : entry[1];
|
|
if (v !== unset && (v !== undefined || keyImpl.preserveUndefined))
|
|
inner[k] = v;
|
|
}
|
|
if (impl.reduce && !ctx.prereduced) {
|
|
const reduced = impl.reduce(inner, ctx.$);
|
|
if (reduced) {
|
|
if (reduced instanceof Disjoint)
|
|
return reduced.throw();
|
|
// we can't cache this reduction for now in case the reduction involved
|
|
// impliedSiblings
|
|
return withMeta(reduced, meta);
|
|
}
|
|
}
|
|
const node = createNode({
|
|
id: ctx.id,
|
|
kind: ctx.kind,
|
|
inner,
|
|
meta,
|
|
$: ctx.$
|
|
});
|
|
return node;
|
|
};
|
|
export const createNode = ({ id, kind, inner, meta, $, ignoreCache }) => {
|
|
const impl = nodeImplementationsByKind[kind];
|
|
const innerEntries = entriesOf(inner);
|
|
const children = [];
|
|
let innerJson = {};
|
|
for (const [k, v] of innerEntries) {
|
|
const keyImpl = impl.keys[k];
|
|
const serialize = keyImpl.serialize ??
|
|
(keyImpl.child ? serializeListableChild : defaultValueSerializer);
|
|
innerJson[k] = serialize(v);
|
|
if (keyImpl.child === true) {
|
|
const listableNode = v;
|
|
if (isArray(listableNode))
|
|
children.push(...listableNode);
|
|
else
|
|
children.push(listableNode);
|
|
}
|
|
else if (typeof keyImpl.child === "function")
|
|
children.push(...keyImpl.child(v));
|
|
}
|
|
if (impl.finalizeInnerJson)
|
|
innerJson = impl.finalizeInnerJson(innerJson);
|
|
let json = { ...innerJson };
|
|
let metaJson = {};
|
|
if (!isEmptyObject(meta)) {
|
|
metaJson = flatMorph(meta, (k, v) => [
|
|
k,
|
|
k === "examples" ? v : defaultValueSerializer(v)
|
|
]);
|
|
json.meta = possiblyCollapse(metaJson, "description", true);
|
|
}
|
|
innerJson = possiblyCollapse(innerJson, impl.collapsibleKey, false);
|
|
const innerHash = JSON.stringify({ kind, ...innerJson });
|
|
json = possiblyCollapse(json, impl.collapsibleKey, false);
|
|
const collapsibleJson = possiblyCollapse(json, impl.collapsibleKey, true);
|
|
const hash = JSON.stringify({ kind, ...json });
|
|
// we have to wait until after reduction to return a cached entry,
|
|
// since reduction can add impliedSiblings
|
|
if ($.nodesByHash[hash] && !ignoreCache)
|
|
return $.nodesByHash[hash];
|
|
const attachments = {
|
|
id,
|
|
kind,
|
|
impl,
|
|
inner,
|
|
innerEntries,
|
|
innerJson,
|
|
innerHash,
|
|
meta,
|
|
metaJson,
|
|
json,
|
|
hash,
|
|
collapsibleJson: collapsibleJson,
|
|
children
|
|
};
|
|
if (kind !== "intersection") {
|
|
for (const k in inner)
|
|
if (k !== "in" && k !== "out")
|
|
attachments[k] = inner[k];
|
|
}
|
|
const node = new nodeClassesByKind[kind](attachments, $);
|
|
return ($.nodesByHash[hash] = node);
|
|
};
|
|
export const withId = (node, id) => {
|
|
if (node.id === id)
|
|
return node;
|
|
if (isNode(nodesByRegisteredId[id]))
|
|
throwInternalError(`Unexpected attempt to overwrite node id ${id}`);
|
|
// have to ignore cache to force creation of new potentially cyclic id
|
|
return createNode({
|
|
id,
|
|
kind: node.kind,
|
|
inner: node.inner,
|
|
meta: node.meta,
|
|
$: node.$,
|
|
ignoreCache: true
|
|
});
|
|
};
|
|
export const withMeta = (node, meta, id) => {
|
|
if (id && isNode(nodesByRegisteredId[id]))
|
|
throwInternalError(`Unexpected attempt to overwrite node id ${id}`);
|
|
return createNode({
|
|
id: id ?? registerNodeId(meta.alias ?? node.kind),
|
|
kind: node.kind,
|
|
inner: node.inner,
|
|
meta,
|
|
$: node.$
|
|
});
|
|
};
|
|
const possiblyCollapse = (json, toKey, allowPrimitive) => {
|
|
const collapsibleKeys = Object.keys(json);
|
|
if (collapsibleKeys.length === 1 && collapsibleKeys[0] === toKey) {
|
|
const collapsed = json[toKey];
|
|
if (allowPrimitive)
|
|
return collapsed;
|
|
if (
|
|
// if the collapsed value is still an object
|
|
hasDomain(collapsed, "object") &&
|
|
// and the JSON did not include any implied keys
|
|
(Object.keys(collapsed).length === 1 || Array.isArray(collapsed))) {
|
|
// we can replace it with its collapsed value
|
|
return collapsed;
|
|
}
|
|
}
|
|
return json;
|
|
};
|