Add queryable capability contracts and checked GraphQL artifacts

This commit is contained in:
Timothy J. Aveni
2026-09-17 14:55:10 -07:00
parent e3501772c5
commit 61a410f98f
27 changed files with 4798 additions and 2333 deletions
+7 -1
View File
@@ -87,11 +87,17 @@ export const instantiateInterface = (
const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) };
switch (member.kind) {
case "value":
return { ...common, kind: "value", valueType: substitution.value(member.valueType, member.displayName) };
return {
...common,
kind: "value",
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
valueType: substitution.value(member.valueType, member.displayName),
};
case "relationship":
return {
...common,
kind: "relationship",
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
target: substitution.object(member.target, member.displayName),
cardinality: member.cardinality,
ordered: member.ordered,
+9
View File
@@ -116,6 +116,8 @@ export interface InterfaceOperation<Type = ValueType> {
eventType?: Type;
}
export type QueryReadContract = { execution: "native" | "rpc-permitted" };
interface InterfaceMemberBase<Type = ValueType> {
id: MemberId;
displayName: string;
@@ -124,6 +126,7 @@ interface InterfaceMemberBase<Type = ValueType> {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value";
queryRead?: QueryReadContract;
valueType: Type;
}
@@ -141,6 +144,7 @@ export interface RelationshipInterfaceMember<
Target = EdgeEndpointConstraint,
> extends InterfaceMemberBase<Type> {
kind: "relationship";
queryRead?: QueryReadContract;
target: Target;
cardinality: EdgeCardinality;
ordered: boolean;
@@ -270,6 +274,8 @@ export interface PackageConstructorExport extends PackageExportBase {
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
export interface PackageRevision {
queries?: import("../query/types.js").QueryDeclaration[];
checkedQueries?: import("../query/types.js").CheckedQuery[];
genericExports?: import("./generic-packages.js").GenericPackageExport[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
migrationCatalog?: import("./migrations.js").MigrationCatalog;
@@ -342,6 +348,8 @@ export type Binding =
export interface OperationBinding {
operationId: OperationId;
binding: Binding;
/** Required for package-backed reads of an RPC-permitted queryable field. */
queryReason?: string;
}
export interface Conformance {
@@ -363,6 +371,7 @@ export interface AtomConstructorBinding {
}
export interface WorkspaceRevision {
linkedQueries?: import("../query/link.js").LinkedQuery[];
id: WorkspaceRevisionId;
workspaceId: WorkspaceId;
parentRevisionIds: WorkspaceRevisionId[];
+42
View File
@@ -40,6 +40,8 @@ import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode =
| "invalid-query-contract"
| "query-provider-reason-required"
| "invalid-semantic-major"
| "duplicate-conformance-id"
| "required-value"
@@ -502,6 +504,21 @@ const collectIdentityIndexes = (
const operations = new Map<OperationId, InterfaceOperationEntry>();
for (const [memberIndex, member] of revision.members.entries()) {
const memberPath = `${path}.members[${memberIndex}]`;
if (member.kind !== "operation" && member.queryRead) {
const contract = member.queryRead.execution;
const getter = member.kind === "value" ? "get" : "resolve";
if (contract !== "native" && contract !== "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "Unknown query execution contract");
if (member.operations.filter((operation) => operation.displayName === getter).length !== 1)
issue(issues, "invalid-query-contract", memberPath, `Queryable members require exactly one ${getter}`);
if (member.kind === "relationship" && contract === "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
if (member.kind === "value") {
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
if (type.kind !== "scalar")
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar");
}
}
requireText(issues, member.id, `${memberPath}.id`, "Member ID");
requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name");
if (memberIds.has(member.id)) {
@@ -1424,6 +1441,31 @@ const validateConformances = (
}
const operation = operationEntry.operation;
const binding = entry.binding;
const member = operationEntry.member;
if (
member.kind !== "operation" &&
member.queryRead &&
operation.displayName === (member.kind === "value" ? "get" : "resolve")
) {
const native =
member.kind === "value"
? binding.kind === "state" && binding.primitive === "read"
: binding.kind === "edge" && binding.primitive === "resolve";
if (!native && (member.queryRead.execution === "native" || binding.kind !== "package"))
issue(
issues,
"invalid-query-contract",
bindingPath,
"This queryable contract requires a native read binding; a justification cannot weaken it",
);
else if (!native && !entry.queryReason?.trim())
issue(
issues,
"query-provider-reason-required",
bindingPath,
"Package-backed query reads require a concrete query-reason justification",
);
}
if (operation.scope === "class" && (!conformance.id || operation.mode !== "call")) {
issue(
issues,