import { createHash } from "node:crypto"; import type { WorkspaceRevision, Binding, AtomId, InterfaceRevisionId, MemberId, PersistentAttachment, } from "../capability-model/types.js"; import { QueryCompileError, type CheckedQuery } from "./types.js"; import { queryRuntimePlan } from "./proto.js"; import { canonicalJson } from "../capability-model/evolution.js"; import { resolveForwardedBinding } from "../capability-model/forwarding.js"; export interface LinkedQueryField { atomId: AtomId; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId; getter: string; binding: Binding; watch?: { start: string; stop: string; binding: Binding }; } export interface LinkedQuery { runtime?: import("@bufbuild/protobuf").JsonValue; id: string; packageRevisionId: string; checked: CheckedQuery; bindingDigest: string; fields: LinkedQueryField[]; attachments: PersistentAttachment[]; rootAtoms: AtomId[]; } /** Link every possible implementation, including atoms absent from today's data. */ export function linkQueries( workspace: WorkspaceRevision, additional: readonly { packageRevisionId: string; checked: CheckedQuery; }[] = [], ): LinkedQuery[] { const interfaces = new Map(workspace.interfaceImports.map((entry) => [entry.revisionId, entry])); const attachments = [ ...workspace.sharedAttachments, ...workspace.conformances.flatMap((entry) => entry.privateAttachments), ]; const linked: LinkedQuery[] = []; for (const pkg of [ ...workspace.packageImports, ...additional.map(({ packageRevisionId, checked }) => ({ revisionId: packageRevisionId, displayName: "Public query", queries: [checked.declaration], checkedQueries: [checked], })), ]) for (const declaration of pkg.queries ?? []) { const checked = pkg.checkedQueries?.find((query) => query.declaration.id === declaration.id); if (!checked || JSON.stringify(checked.declaration) !== JSON.stringify(declaration)) throw new QueryCompileError( "QUERY_ARTIFACT_MISSING", `${pkg.displayName}.${declaration.displayName} has no checked source artifact`, ); const fields: LinkedQueryField[] = []; const needed = new Set(); for (const view of declaration.views) if ( !workspace.conformances.some( (entry) => entry.atomId === view.atomId && entry.interfaceRevisionId === view.interfaceRevisionId, ) ) throw new QueryCompileError( "QUERY_VIEW_REQUIRED", `${view.atomId} does not conform to declared query view ${view.interfaceRevisionId}`, ); for (const effect of checked.effects) { const contract = interfaces.get(effect.interfaceRevisionId); const member = contract?.members.find((entry) => entry.id === effect.memberId); if ( !member || member.kind === "operation" || member.kind === "module" || member.queryRead?.execution !== effect.execution ) throw new QueryCompileError( "QUERY_STALE_CONTRACT", `Query field ${effect.interfaceRevisionId}.${effect.memberId} changed`, ); const getter = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve")); if (!getter) throw new QueryCompileError("QUERY_CONTRACT", `Missing getter ${effect.memberId}`); for (const conformance of workspace.conformances.filter( (entry) => entry.interfaceRevisionId === effect.interfaceRevisionId, )) { const provider = conformance.operationBindings.find((entry) => entry.operationId === getter.id); if (!provider) throw new QueryCompileError("QUERY_CONTRACT", `Missing binding ${conformance.atomId}.${getter.id}`); const binding = resolveForwardedBinding( workspace, conformance.atomId, conformance.interfaceRevisionId, getter.id, ).binding; if (binding.kind === "package") { if (effect.execution === "native") throw new QueryCompileError( "QUERY_NATIVE_BINDING_REQUIRED", `Native query field ${effect.memberId} binds package code`, ); if (effect.execution === "rpc-permitted" && !provider.queryReason?.trim()) throw new QueryCompileError( "QUERY_RPC_PROVIDER_REASON", `Package query field ${effect.memberId} needs query-reason`, ); } else if (binding.kind === "state" && binding.primitive === "read") needed.add(binding.slotId); else if ( (binding.kind === "journal" && binding.primitive === "head") || (binding.kind === "subscription" && binding.primitive === "status") ) needed.add(binding.slotId); else if (binding.kind === "edge" && binding.primitive === "resolve") needed.add(binding.edgeTypeId); else throw new QueryCompileError( "QUERY_NATIVE_BINDING_REQUIRED", `Unsupported query read binding ${effect.memberId}`, ); const start = member.operations.find((op) => op.displayName === "watch-start"); const stop = member.operations.find((op) => op.displayName === "watch-stop"); const watch = start && stop && conformance.operationBindings.some((entry) => entry.operationId === start.id) ? resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id) : undefined; if ( binding.kind === "state" && watch && (watch.binding.kind !== "state" || watch.binding.slotId !== binding.slotId || watch.binding.primitive !== "watch-start") ) throw new QueryCompileError( "QUERY_WATCH_CONTRACT", `Native query field ${effect.memberId} watch disagrees with its getter`, ); if ( binding.kind === "edge" && watch && (watch.binding.kind !== "edge" || watch.binding.edgeTypeId !== binding.edgeTypeId || watch.binding.projectionId !== binding.projectionId) ) throw new QueryCompileError( "QUERY_WATCH_CONTRACT", `Native query relation ${effect.memberId} watch disagrees with its resolver`, ); fields.push({ atomId: conformance.atomId, interfaceRevisionId: effect.interfaceRevisionId, memberId: effect.memberId, getter: getter.id, binding, ...(watch && start && stop ? { watch: { start: start.id, stop: stop.id, binding: watch.binding } } : {}), }); } } const storage = attachments.filter((entry) => needed.has(entry.id)); const rootConformances = workspace.conformances.filter( (entry) => entry.interfaceRevisionId === declaration.root.interface, ); const rootAtoms = [...new Set(rootConformances.map((entry) => entry.atomId))].sort(); const qualifiedInterfaces = new Set(); const qualifications = (node: unknown): void => { if (!node || typeof node !== "object") return; if ("kind" in node && node.kind === "as" && "interfaceRevisionId" in node) qualifiedInterfaces.add(String(node.interfaceRevisionId)); for (const child of Object.values(node)) qualifications(child); }; qualifications(checked.selection); const bindingDigest = createHash("sha256") .update( canonicalJson({ definition: checked.definitionDigest, qualifiedConformances: workspace.conformances .filter((entry) => qualifiedInterfaces.has(entry.interfaceRevisionId)) .map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId })) .sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))), rootConformances: rootConformances .map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId })) .sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))), fields: [...fields].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))), storage: [...storage].sort((a, b) => a.id.localeCompare(b.id)), }), ) .digest("hex"); linked.push({ id: `${pkg.revisionId}:${declaration.id}`, packageRevisionId: pkg.revisionId, checked, bindingDigest, fields, attachments: storage, rootAtoms, }); } for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace); if (new Set(linked.map((entry) => entry.id)).size !== linked.length) throw new QueryCompileError( "QUERY_ID_COLLISION", "Query export identities collide; choose distinct package/query IDs", ); return linked; }