Add queryable capability contracts and checked GraphQL artifacts
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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";
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
/** 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}`);
|
||||
const binding = provider.binding;
|
||||
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");
|
||||
const watch = start && stop && conformance.operationBindings.find((entry) => entry.operationId === start.id);
|
||||
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 bindingDigest = createHash("sha256")
|
||||
.update(JSON.stringify({ definition: checked.definitionDigest, fields, storage }))
|
||||
.digest("hex");
|
||||
linked.push({
|
||||
id: `${pkg.revisionId}:${declaration.id}`,
|
||||
packageRevisionId: pkg.revisionId,
|
||||
checked,
|
||||
bindingDigest,
|
||||
fields,
|
||||
attachments: storage,
|
||||
});
|
||||
}
|
||||
for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace);
|
||||
return linked;
|
||||
}
|
||||
Reference in New Issue
Block a user