import { GraphQLBoolean, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLScalarType, GraphQLObjectType, GraphQLInputObjectType, GraphQLEnumType, GraphQLList, GraphQLNonNull, GraphQLSchema, GraphQLDirective, DirectiveLocation, specifiedDirectives, type GraphQLOutputType, type GraphQLInputType, type GraphQLFieldConfigMap, type GraphQLInputFieldConfigMap, type DocumentNode, } from "graphql"; import { createHash } from "node:crypto"; import type { InterfaceRevision, InterfaceRevisionId, ValueType, RelationshipInterfaceMember, } from "../capability-model/types.js"; import { QueryCompileError, type QueryDeclaration } from "./types.js"; import { relationalSchema, objectRow } from "./relational-schema.js"; // Validate losslessly; callers carry decimal strings across JSON transports. function integerParser(name: string, min: bigint, max: bigint, value: unknown): string { if ( typeof value !== "string" && typeof value !== "bigint" && !(typeof value === "number" && Number.isSafeInteger(value)) ) throw new Error(`${name} requires a canonical integer`); const text = String(value); if (!/^-?(0|[1-9][0-9]*)$/.test(text) || text === "-0") throw new Error(`${name} requires a canonical integer`); const n = BigInt(text); if (n < min || n > max) throw new Error(`${name} out of range`); return text; } const integerScalar = (name: string, min: bigint, max: bigint) => { const parse = (v: unknown) => integerParser(name, min, max, v); return new GraphQLScalarType({ name, serialize: parse, parseValue: parse, parseLiteral(node) { if (node.kind !== "IntValue" && node.kind !== "StringValue") throw new Error(`${name} requires an integer`); return parse(node.value); }, }); }; export const queryScalars = { bool: GraphQLBoolean, string: GraphQLString, int32: GraphQLInt, int64: integerScalar("Int64", -(1n << 63n), (1n << 63n) - 1n), uint32: integerScalar("UInt32", 0n, (1n << 32n) - 1n), uint64: integerScalar("UInt64", 0n, (1n << 64n) - 1n), double: GraphQLFloat, bytes: new GraphQLScalarType({ name: "Bytes" }), }; export const cursorScalar = new GraphQLScalarType({ name: "Cursor", parseValue(value) { if (typeof value !== "string" || value.length > 4096) throw new Error("Invalid cursor"); return value; }, }); export function querySchema( declaration: QueryDeclaration, interfaces: readonly InterfaceRevision[], document?: DocumentNode, ) { const contracts = new Map(interfaces.map((entry) => [entry.revisionId, entry])); const objects = new Map(); const orders = new Map(); const byName = new Map(); const comparisons = new Map(); const scalarTypes = new Map( Object.entries(queryScalars).map(([name, type]) => [type.name, { kind: "scalar", name } as ValueType]), ); scalarTypes.set("Cursor", { kind: "scalar", name: "string" }); const pageInfo = new GraphQLObjectType({ name: "QxPageInfo", fields: { hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) }, endCursor: { type: cursorScalar }, }, }); const direction = new GraphQLEnumType({ name: "QxDirection", values: { ASC: {}, DESC: {} } }); const contract = (id: InterfaceRevisionId) => { const found = contracts.get(id); if (!found || found.template) throw new QueryCompileError("QUERY_CONTRACT", `Expected closed interface ${id}`); return found; }; const name = (id: InterfaceRevisionId) => { const revision = contract(id); const result = revision.displayName + (revision.application ? `_${createHash("sha256").update(id).digest("hex").slice(0, 12)}` : ""); if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(result) || result.startsWith("__") || result.startsWith("Qx")) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query interface name ${result}`); const existing = byName.get(result); if (existing && existing.revisionId !== id) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Conflicting query interface name ${result}`); byName.set(result, revision); return result; }; const target = (member: RelationshipInterfaceMember): InterfaceRevisionId => { if (member.target.kind === "interface") return member.target.interfaceRevisionId; const atomId = member.target.atomId; const views = declaration.views.filter((entry) => entry.atomId === atomId); if (views.length !== 1) throw new QueryCompileError( "QUERY_VIEW_REQUIRED", `Declare exactly one interface view for ${member.target.atomId}`, ); return views[0]!.interfaceRevisionId; }; const scalar = (type: ValueType): GraphQLOutputType & GraphQLInputType => { if (type.kind === "optional") return scalar(type.value); if (type.kind !== "scalar") throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported"); return queryScalars[type.name]; }; const comparison = (type: ValueType) => { const base = type.kind === "optional" ? type.value : type; const value = scalar(base); const key = String(value); let result = comparisons.get(key); if (!result) { const fields: GraphQLInputFieldConfigMap = { eq: { type: value }, isNull: { type: GraphQLBoolean } }; if (base.kind === "scalar" && base.name !== "bytes") fields.in = { type: new GraphQLList(new GraphQLNonNull(value)) }; if (base.kind === "scalar" && base.name !== "bool" && base.name !== "bytes") for (const op of ["lt", "lte", "gt", "gte"]) fields[op] = { type: value }; result = new GraphQLInputObjectType({ name: `QxCompare${key}`, fields }); comparisons.set(key, result); } return result; }; const relational = relationalSchema( { contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo }, document, ); relational.discover(declaration.root); const filter = (id: InterfaceRevisionId) => relational.where(objectRow(id)); const order = (id: InterfaceRevisionId) => { let result = orders.get(id); if (!result) { const fields: GraphQLInputFieldConfigMap = {}; for (const member of contract(id).members) { if (member.kind !== "value" || !member.queryRead) continue; const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType; if (t.kind === "scalar" && t.name !== "bytes") fields[member.displayName] = { type: direction }; } if (Object.keys(fields).length === 0) return undefined; result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields }); orders.set(id, result); } return result; }; const object = (id: InterfaceRevisionId): GraphQLObjectType => { const existing = objects.get(id); if (existing) return existing; const result = new GraphQLObjectType({ name: name(id), fields: () => { const helpers = relational.helpers(id); const metadata = new GraphQLObjectType({ name: `QxMetadata_${name(id)}`, fields: { ref: { type: new GraphQLNonNull(relational.refs(id)) }, ...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}), }, }); const fields: GraphQLFieldConfigMap = { _qx: { type: new GraphQLNonNull(metadata) } }; for (const member of contract(id).members) { if (member.kind === "operation" || !member.queryRead) continue; if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_")) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`); if (member.kind === "value") { const type = scalar(member.valueType); fields[member.displayName] = { type: member.valueType.kind === "optional" ? type : new GraphQLNonNull(type), }; } else { const targetId = target(member), node = object(targetId); if (member.cardinality === "exactly-one" || member.cardinality === "optional-one") fields[member.displayName] = { type: member.cardinality === "exactly-one" ? new GraphQLNonNull(node) : node, }; else { const entry = new GraphQLObjectType({ name: `${name(id)}_${member.displayName}_Entry`, fields: { key: { type: new GraphQLNonNull(GraphQLString) }, ...(member.keyType ? { mapKey: { type: new GraphQLNonNull( queryScalars[member.keyType === "boolean" ? "bool" : member.keyType], ), }, } : {}), cursor: { type: cursorScalar }, node: { type: new GraphQLNonNull(node) }, }, }); const connection = new GraphQLObjectType({ name: `${name(id)}_${member.displayName}_Connection`, fields: { entries: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(entry))) }, pageInfo: { type: new GraphQLNonNull(pageInfo) }, }, }); const ordering = order(targetId); fields[member.displayName] = { type: new GraphQLNonNull(connection), args: { first: { type: GraphQLInt }, all: { type: GraphQLInt }, after: { type: cursorScalar }, where: { type: filter(targetId) }, ...(ordering ? { orderBy: { type: new GraphQLList(new GraphQLNonNull(ordering)) } } : {}), }, }; } } } return fields; }, }); objects.set(id, result); return result; }; const schema = new GraphQLSchema({ directives: [...specifiedDirectives, new GraphQLDirective({ name: "live", locations: [DirectiveLocation.FIELD] })], query: new GraphQLObjectType({ name: "QxQuery", fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } }, }), }); return { schema, byName, target, contracts, relational, scalarTypes }; }