Add queryable capability contracts and checked GraphQL artifacts
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user