2026-09-17 14:34:16 -07:00
|
|
|
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";
|
2026-09-17 14:34:16 -07:00
|
|
|
import { canonicalJson } from "../capability-model/evolution.js";
|
2026-09-18 16:59:19 -07:00
|
|
|
import { resolveForwardedBinding } from "../capability-model/forwarding.js";
|
2026-09-17 14:34:16 -07:00
|
|
|
|
|
|
|
|
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[];
|
2026-09-18 14:55:12 -07:00
|
|
|
rootAtoms: AtomId[];
|
2026-09-17 14:34:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Link every possible implementation, including atoms absent from today's data. */
|
|
|
|
|
export function linkQueries(workspace: WorkspaceRevision): 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)
|
|
|
|
|
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<string>();
|
|
|
|
|
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.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}`);
|
2026-09-18 16:59:19 -07:00
|
|
|
const binding = resolveForwardedBinding(
|
|
|
|
|
workspace,
|
|
|
|
|
conformance.atomId,
|
|
|
|
|
conformance.interfaceRevisionId,
|
|
|
|
|
getter.id,
|
|
|
|
|
).binding;
|
2026-09-17 14:34:16 -07:00
|
|
|
if (binding.kind === "package") {
|
|
|
|
|
if (effect.execution !== "rpc-permitted")
|
|
|
|
|
throw new QueryCompileError(
|
|
|
|
|
"QUERY_NATIVE_BINDING_REQUIRED",
|
|
|
|
|
`Native query field ${effect.memberId} binds package code`,
|
|
|
|
|
);
|
|
|
|
|
if (!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 === "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");
|
2026-09-18 16:59:19 -07:00
|
|
|
const watch =
|
|
|
|
|
start && stop && conformance.operationBindings.some((entry) => entry.operationId === start.id)
|
|
|
|
|
? resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id)
|
|
|
|
|
: undefined;
|
2026-09-17 14:34:16 -07:00
|
|
|
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));
|
2026-09-18 14:55:12 -07:00
|
|
|
const rootConformances = workspace.conformances.filter(
|
|
|
|
|
(entry) => entry.interfaceRevisionId === declaration.root.interface,
|
|
|
|
|
);
|
|
|
|
|
const rootAtoms = [...new Set(rootConformances.map((entry) => entry.atomId))].sort();
|
2026-09-17 14:34:16 -07:00
|
|
|
const bindingDigest = createHash("sha256")
|
2026-09-17 14:34:16 -07:00
|
|
|
.update(
|
|
|
|
|
canonicalJson({
|
|
|
|
|
definition: checked.definitionDigest,
|
2026-09-18 14:55:12 -07:00
|
|
|
rootConformances: rootConformances
|
|
|
|
|
.map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId }))
|
|
|
|
|
.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
|
2026-09-17 14:34:16 -07:00
|
|
|
fields: [...fields].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
|
|
|
|
|
storage: [...storage].sort((a, b) => a.id.localeCompare(b.id)),
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-09-17 14:34:16 -07:00
|
|
|
.digest("hex");
|
|
|
|
|
linked.push({
|
|
|
|
|
id: `${pkg.revisionId}:${declaration.id}`,
|
|
|
|
|
packageRevisionId: pkg.revisionId,
|
|
|
|
|
checked,
|
|
|
|
|
bindingDigest,
|
|
|
|
|
fields,
|
|
|
|
|
attachments: storage,
|
2026-09-18 14:55:12 -07:00
|
|
|
rootAtoms,
|
2026-09-17 14:34:16 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace);
|
2026-09-17 14:34:16 -07:00
|
|
|
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",
|
|
|
|
|
);
|
2026-09-17 14:34:16 -07:00
|
|
|
return linked;
|
|
|
|
|
}
|