Files
quixos-protocol/src/bindings/presentation-queries.ts
T

84 lines
4.2 KiB
TypeScript
Raw Normal View History

import { createHash } from "node:crypto";
import type { InterfaceRevision, PackageRevision, ValueType } from "../capability-model/types.js";
import { defaultQueryBudgets, type QueryDeclaration } from "../query/types.js";
/** React contract tooling generates ordinary queries before normal checking.
* Neither the query language nor its execution planner knows presentation IDs. */
export function presentationQueries(pkg: PackageRevision, interfaces: readonly InterfaceRevision[]) {
const documents = new Map<string, string>();
const generated: QueryDeclaration[] = [];
const contracts = new Map(interfaces.map((contract) => [contract.revisionId, contract]));
const navigation = new Set<string>();
for (const module of pkg.modules ?? []) {
const contract = contracts.get(module.interfaceRevisionId);
const member = contract?.members.find((member) => member.id === module.memberId);
if (member?.kind !== "module" || member.contract !== "org.quixos.react.component/2") continue;
const argument = contract?.application?.arguments[1];
if (argument?.kind !== "object" || argument.target.kind !== "interface")
throw Error("Stateful React presentations require a closed navigation interface");
navigation.add(argument.target.interfaceRevisionId);
}
const valueSelection = (type: ValueType): string => {
if (type.kind === "optional" || type.kind === "list") return valueSelection(type.value);
return type.kind === "record"
? `{${Object.entries(type.fields)
.map(([name, type]) => `${name} ${valueSelection(type)}`)
.join(" ")}}`
: "";
};
for (const id of [...navigation].sort()) {
const contract = contracts.get(id as InterfaceRevision["revisionId"]);
if (!contract || contract.template) throw Error(`Missing closed presentation navigation ${id}`);
const fields = contract.members.flatMap((member) => {
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
if (member.kind === "value") return [`${member.displayName} ${valueSelection(member.valueType)}`];
if (member.target.kind !== "interface")
throw Error(`Navigation relationship ${member.displayName} must declare an interface view`);
return [
member.cardinality === "many" || member.cardinality === "many-unique"
? `${member.displayName}(all:512) {entries {node {_camino {ref}}}}`
: `${member.displayName} {_camino {ref}}`,
];
});
const stem = `PresentationNavigation${createHash("sha256").update(id).digest("hex").slice(0, 12)}`;
const inputs = `presentation:navigation:${id}:inputs`;
for (const local of [false, true]) {
const name = `${stem}${local ? "View" : "Inputs"}`;
const document = `generated/${name}.graphql`;
documents.set(
document,
`query ${name}($presentationMount: QxRef_PresentationMount!) {
root { ${local ? "availableObjects" : "objects"}(all:512, where:{_camino:{as:{PresentationState:{presentationMount:{eq:$presentationMount}}}}}) {
entries { node { ${fields.join("\n")}
... on PresentationState {
_presentationOccurrence: occurrence
_presentationSource: source {_camino {ref}}
_presentationParent: parent {_camino {ref}}
_presentationMount: presentationMount {_camino {ref}}
}
} }
} }
}`,
);
generated.push({
id: local ? `presentation:navigation:${id}:view` : inputs,
displayName: name,
root: { kind: "workspace-interface", interface: contract.revisionId },
document,
operation: name,
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets, rows: 5000, candidates: 5000, resultBytes: 8388608 },
watch: local,
placement: local ? { kind: "local", sourceQueryId: inputs } : { kind: "replica" },
});
}
}
for (const query of generated)
if (pkg.queries?.some((existing) => existing.id === query.id || existing.displayName === query.displayName))
throw Error(`Presentation query conflicts with an authored query: ${query.id}`);
pkg.queries = [...(pkg.queries ?? []), ...generated];
return documents;
}