95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import type { CheckedQuery, QueryTreeNode } from "../query/types.js";
|
|
import type { ValueType } from "../capability-model/types.js";
|
|
import { treeProjectionQuery } from "../query/tree.js";
|
|
import { tsType } from "../bindings/portable.js";
|
|
|
|
export function queryTreeBlueprint(packageId: string, query: CheckedQuery) {
|
|
const tree = query.tree!;
|
|
const projection = (id: string) =>
|
|
JSON.stringify([packageId, treeProjectionQuery(query.declaration.id, id, "local")]);
|
|
const admission = (id: string) =>
|
|
JSON.stringify([packageId, treeProjectionQuery(query.declaration.id, id, "replica")]);
|
|
return {
|
|
root: tree.root,
|
|
maxOccurrences: tree.maxOccurrences,
|
|
nodes: tree.nodes.map((node) => ({
|
|
...node,
|
|
projection: projection(node.projection),
|
|
admission: admission(node.projection),
|
|
bindings: node.bindings.map((binding) => ({
|
|
...binding,
|
|
projection: projection(binding.projection),
|
|
admission: admission(binding.projection),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
|
|
/** Emit the shape of owned handles separately from graph result codecs. */
|
|
export function queryTreeType(query: CheckedQuery): string {
|
|
const tree = query.tree!;
|
|
const nodes = new Map(tree.nodes.map((node) => [node.id, node]));
|
|
const projections = new Map(tree.projections.map((projection) => [projection.id, projection]));
|
|
const rootOutput = (id: string) => {
|
|
const shape = projections.get(id)!.output;
|
|
if (shape.kind !== "record" || !shape.fields.root) throw Error("Unchecked Query Tree projection");
|
|
return shape.fields.root;
|
|
};
|
|
const shape = (id: string): string => {
|
|
const node = nodes.get(id)!;
|
|
return `QueryTreeShape<${tsType(rootOutput(node.projection))}, {${node.boundaries
|
|
.map(
|
|
(boundary) =>
|
|
`readonly ${JSON.stringify(boundary.name)}${boundary.conditions.length ? "?" : ""}: ${shape(boundary.node)}`,
|
|
)
|
|
.join(";")}}, {readonly root: {${node.bindings
|
|
.map(
|
|
(binding) =>
|
|
`readonly ${JSON.stringify(binding.name)}: ${bindingType(rootOutput(binding.projection), binding.targets, [binding.name])}`,
|
|
)
|
|
.join(";")}}}>`;
|
|
};
|
|
const bindingType = (
|
|
type: ValueType,
|
|
targets: QueryTreeNode["bindings"][number]["targets"],
|
|
prefix: string[],
|
|
): string => {
|
|
const at = (type: ValueType, targets: QueryTreeNode["bindings"][number]["targets"]): string => {
|
|
if (type.kind === "optional") return `(${at(type.value, targets)} | null)`;
|
|
if (type.kind === "list")
|
|
return `ReadonlyArray<${at(
|
|
type.value,
|
|
targets.map((target) => ({ ...target, path: target.path.slice(1) })),
|
|
)}>`;
|
|
const target = targets.find((target) => !target.path.length);
|
|
if (target) {
|
|
if (type.kind !== "record" || type.fields._camino?.kind !== "record" || !type.fields._camino.fields.ref)
|
|
throw Error("Unchecked binding identity");
|
|
return `QueryTreeOccurrence<${tsType(type.fields._camino.fields.ref)}, ${shape(target.node)}>`;
|
|
}
|
|
if (type.kind !== "record") return tsType(type);
|
|
return `{${Object.entries(type.fields)
|
|
.map(
|
|
([key, value]) =>
|
|
`readonly ${JSON.stringify(key)}: ${at(
|
|
value,
|
|
targets
|
|
.filter((target) => target.path[0] === key)
|
|
.map((target) => ({ ...target, path: target.path.slice(1) })),
|
|
)}`,
|
|
)
|
|
.join(";")}}`;
|
|
};
|
|
let selected = type;
|
|
for (const key of prefix) {
|
|
if (selected.kind !== "record" || !selected.fields[key]) throw Error("Unchecked binding field");
|
|
selected = selected.fields[key];
|
|
}
|
|
return at(
|
|
selected,
|
|
targets.map((target) => ({ ...target, path: target.path.slice(prefix.length) })),
|
|
);
|
|
};
|
|
return shape(tree.root);
|
|
}
|