Generate checked navigation query pairs from closed React contracts before package checking. Retain per-type subscriptions under the logical PresentationMount and resolve cold component metadata independently of code. Keep scoped occurrence membership and idempotent local preparation in the presentation library; copy directly through state contracts without reading other PresentationMounts' source associations. Preserve normal transaction creation, pending references, portal synchronization and detach. Exercise automatic object refs in Reader queries, imported fragments and structured/aggregate results. Add heterogeneous owner-index, live dependency, cold metadata, browser ownership and latency coverage. Document measured improvements, supported query limits and qualification evidence.
84 lines
4.2 KiB
TypeScript
84 lines
4.2 KiB
TypeScript
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;
|
|
}
|