import { GraphQLBoolean, GraphQLString, GraphQLInt, GraphQLScalarType, GraphQLObjectType, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, type GraphQLInputType, type GraphQLOutputType, type GraphQLInputFieldConfigMap, type GraphQLFieldConfigMap, type GraphQLEnumType, type DocumentNode, type SelectionSetNode, type FragmentDefinitionNode, } from "graphql"; import { createHash } from "node:crypto"; import { valueType, type ValueType, type InterfaceRevision, type InterfaceRevisionId, type RelationshipInterfaceMember, } from "../capability-model/types.js"; import { aggregateType, aggregateOperators, queryKeyType, type QueryRow, type AggregateOperator, } from "./relational.js"; import { QueryCompileError } from "./types.js"; export const rowKey = (row: QueryRow): string => JSON.stringify(row); export const objectRow = ( interfaceRevisionId: InterfaceRevisionId, membership?: { keyType?: "string" | "boolean" | "int64" }, ): QueryRow => ({ kind: "object", interfaceRevisionId, ...(membership ? { membership } : {}) }); export type SchemaRowField = { kind: "leaf"; type: ValueType } | { kind: "row"; row: QueryRow; optional: boolean }; export interface RelationalSchemaEnvironment { contracts: Map; target(member: RelationshipInterfaceMember): InterfaceRevisionId; name(id: InterfaceRevisionId): string; scalar(type: ValueType): GraphQLInputType & GraphQLOutputType; comparison(type: ValueType): GraphQLInputObjectType; scalarTypes: Map; direction: GraphQLEnumType; cursor: GraphQLScalarType; pageInfo: GraphQLObjectType; } export function relationalSchema(env: RelationalSchemaEnvironment, document?: DocumentNode) { const outputCache = new Map(); const inputCache = new Map(); const referenceCache = new Map(); const rowsets = new Map(); const expansionPaths = new Map>(); const roles = new Map< string, { kind: "rowset" | "aggregate" | "groups" | "keys" | "expand" | "relations"; row: QueryRow } >(); const many = (member: RelationshipInterfaceMember) => member.cardinality === "many" || member.cardinality === "many-unique"; const relationRow = (member: RelationshipInterfaceMember) => objectRow(env.target(member), { keyType: member.keyType }); const relations = (row: QueryRow) => row.kind === "object" ? env.contracts .get(row.interfaceRevisionId)! .members.filter( (member): member is RelationshipInterfaceMember => member.kind === "relationship" && !!member.queryRead, ) : []; const fields = (row: QueryRow): Map => { if (row.kind === "pair") return new Map([ ["source", { kind: "row", row: row.source, optional: false }], ["target", { kind: "row", row: row.target, optional: false }], ]); return new Map( env.contracts.get(row.interfaceRevisionId)!.members.flatMap((member): [string, SchemaRowField][] => { if (member.kind === "operation" || !member.queryRead) return []; if (member.kind === "value") return [[member.displayName, { kind: "leaf", type: member.valueType }]]; if (many(member)) return []; return [ [ member.displayName, { kind: "row", row: objectRow(env.target(member)), optional: member.cardinality === "optional-one" }, ], ]; }), ); }; const refs = (id: InterfaceRevisionId) => { let result = referenceCache.get(id); if (!result) { result = new GraphQLScalarType({ name: `QxRef_${env.name(id)}`, parseValue: (value) => value, parseLiteral() { throw new Error("Managed references must be supplied as typed variables"); }, }); referenceCache.set(id, result); env.scalarTypes.set(result.name, valueType.interfaceRef(id)); } return result; }; const scalar = (type: ValueType): GraphQLInputType & GraphQLOutputType => { if (type.kind === "optional") return scalar(type.value); return type.kind === "object-ref" && type.expectation.kind === "interface" ? refs(type.expectation.interfaceRevisionId) : env.scalar(type); }; const leafFields = (row: QueryRow) => { const result = new Map(); if (row.kind === "object") result.set("ref", valueType.interfaceRef(row.interfaceRevisionId)); if (row.kind === "pair" || row.membership) result.set("entry", valueType.string); if (row.kind === "object" && row.membership?.keyType) result.set( "mapKey", row.membership.keyType === "boolean" ? valueType.bool : row.membership.keyType === "int64" ? valueType.int64 : valueType.string, ); return result; }; const typeName = (role: string, row: QueryRow, suffix = "") => `Qx${role}_${createHash("sha256") .update(rowKey(row) + suffix) .digest("hex") .slice(0, 16)}`; const out = (role: string, row: QueryRow, make: () => GraphQLFieldConfigMap, suffix = "") => { const key = typeName(role, row, suffix); let result = outputCache.get(key); if (!result) { result = new GraphQLObjectType({ name: key, fields: make }); outputCache.set(key, result); } return result; }; const inp = (role: string, row: QueryRow, make: () => GraphQLInputFieldConfigMap, suffix = "") => { const key = typeName(role, row, suffix); let result = inputCache.get(key); if (!result) { result = new GraphQLInputObjectType({ name: key, fields: make }); inputCache.set(key, result); } return result; }; const compare = (type: ValueType) => { const base = type.kind === "optional" ? type.value : type; if (base.kind !== "object-ref") return env.comparison(type); return inp( "RefCompare", objectRow( base.expectation.kind === "interface" ? base.expectation.interfaceRevisionId : (() => { throw new Error("Expected interface reference"); })(), ), () => ({ eq: { type: scalar(type) }, in: { type: new GraphQLList(new GraphQLNonNull(scalar(type))) } }), ); }; // Recursive interface graphs are interned. Synthetic pair graphs are finite and document-driven. const columns = ( row: QueryRow, operator?: Exclude, nullable = false, ): GraphQLObjectType => out( "Columns", row, () => { const result: GraphQLFieldConfigMap = {}; for (const [name, field] of fields(row)) { if (field.kind === "row") result[name] = { type: new GraphQLNonNull(columns(field.row, operator, nullable || field.optional)) }; else { let type = field.type; if (operator) { try { type = aggregateType(operator, type); } catch { continue; } } else if (!queryKeyType(type)) continue; result[name] = { type: type.kind === "optional" || (!operator && nullable) ? scalar(type) : new GraphQLNonNull(scalar(type)), }; } } const metadata: GraphQLFieldConfigMap = {}; for (const [name, type] of leafFields(row)) { if (operator && operator !== "countPresent") continue; const t = scalar(operator ? aggregateType(operator, type) : type); metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) }; } if (Object.keys(metadata).length) result._qx = { type: new GraphQLNonNull(out("ColumnsMeta", row, () => metadata, `${operator}:${nullable}`)) }; // A type may have no matching scalar at this node; a private schema sentinel // keeps it valid while compiler validation forbids selecting that sentinel. if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean }; return result; }, `${operator}:${nullable}`, ); const aggregate = (row: QueryRow): GraphQLObjectType => { const result = out("Aggregate", row, () => Object.fromEntries( aggregateOperators.map((op) => [ op, { type: new GraphQLNonNull(op === "count" ? env.scalar(valueType.uint64) : columns(row, op)) }, ]), ), ); roles.set(result.name, { kind: "aggregate", row }); return result; }; const keys = (row: QueryRow): GraphQLInputObjectType => inp("Keys", row, () => { const result: GraphQLInputFieldConfigMap = {}; for (const [name, field] of fields(row)) { if (field.kind === "row") result[name] = { type: keys(field.row) }; else if (queryKeyType(field.type)) result[name] = { type: GraphQLBoolean }; } const meta = Object.fromEntries([...leafFields(row)].map(([name]) => [name, { type: GraphQLBoolean }])); if (Object.keys(meta).length) result._qx = { type: inp("KeyMeta", row, () => meta) }; return result; }); const comparisons = ( row: QueryRow, operator?: Exclude, ordering = false, ): GraphQLInputObjectType => inp( "Expressions", row, () => { const result: GraphQLInputFieldConfigMap = {}; for (const [name, field] of fields(row)) { if (field.kind === "row") result[name] = { type: comparisons(field.row, operator, ordering) }; else { let type = field.type; if (operator) { try { type = aggregateType(operator, type); } catch { continue; } } result[name] = { type: ordering ? env.direction : compare(type) }; } } const meta: GraphQLInputFieldConfigMap = {}; for (const [name, type] of leafFields(row)) if (!operator || operator === "countPresent") meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) }; if (Object.keys(meta).length) result._qx = { type: inp("ExpressionMeta", row, () => meta, `${operator}:${ordering}`) }; if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean }; return result; }, `${operator}:${ordering}`, ); const having = (row: QueryRow, ordering = false): GraphQLInputObjectType => inp( "Having", row, () => { const result: GraphQLInputFieldConfigMap = { group: { type: comparisons(row, undefined, ordering) } }; for (const op of aggregateOperators) result[op] = { type: op === "count" ? (ordering ? env.direction : compare(valueType.uint64)) : comparisons(row, op, ordering), }; if (!ordering) Object.assign(result, { and: { type: new GraphQLList(new GraphQLNonNull(having(row))) }, or: { type: new GraphQLList(new GraphQLNonNull(having(row))) }, not: { type: having(row) }, }); return result; }, String(ordering), ); const where = (row: QueryRow): GraphQLInputObjectType => inp("Where", row, () => { const result: GraphQLInputFieldConfigMap = { and: { type: new GraphQLList(new GraphQLNonNull(where(row))) }, or: { type: new GraphQLList(new GraphQLNonNull(where(row))) }, not: { type: where(row) }, }; for (const [name, field] of fields(row)) { if (row.kind === "object" && field.kind === "row") continue; if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`); result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) }; } result._qx = { type: inp("WhereMeta", row, () => { const meta: GraphQLInputFieldConfigMap = {}; for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) }; const edges = relations(row); if (edges.length) meta.relations = { type: inp("WhereRelations", row, () => Object.fromEntries( edges.map((member) => { const target = relationRow(member); return [ member.displayName, { type: inp( "RelationPredicate", target, (): GraphQLInputFieldConfigMap => many(member) ? { some: { type: where(target) }, none: { type: where(target) }, aggregate: { type: inp("ReductionPredicate", target, () => ({ where: { type: where(target) }, having: { type: new GraphQLNonNull(having(target)) }, })), }, } : { is: { type: where(target) }, isNull: { type: GraphQLBoolean } }, rowKey(row) + member.id, ), }, ]; }), ), ), }; return meta; }), }; return result; }); const fragments = new Map( (document?.definitions ?? []) .filter((d): d is FragmentDefinitionNode => d.kind === "FragmentDefinition") .map((d) => [d.name.value, d]), ); let visited = 0; const selections = (set: SelectionSetNode, active: ReadonlySet = new Set()): import("graphql").FieldNode[] => set.selections.flatMap((node) => { if (++visited > 10000) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query schema discovery exceeds 10000 nodes"); if (node.kind === "Field") return [node]; if (node.kind === "FragmentSpread") { if (active.has(node.name.value) || active.size >= 128) throw new QueryCompileError("QUERY_VALIDATION", "Cyclic or excessively nested fragments"); const fragment = fragments.get(node.name.value); return fragment ? selections(fragment.selectionSet, new Set([...active, node.name.value])) : []; } return []; }); const discoverExpansion = (root: QueryRow, current: QueryRow, set: SelectionSetNode, path: string[] = []) => { for (const node of selections(set)) { if (!node.selectionSet) continue; const member = relations(current).find((m) => m.displayName === node.name.value); const next = [...path, node.name.value]; if (member && many(member)) { const map = expansionPaths.get(rowKey(root)) ?? new Map(); map.set(next.join("."), next); expansionPaths.set(rowKey(root), map); discoverRows({ kind: "pair", source: root, target: relationRow(member) }, node.selectionSet); } else { const field = fields(current).get(node.name.value); if (field?.kind === "row") discoverExpansion(root, field.row, node.selectionSet, next); } } }; const discoverRows = (row: QueryRow, set: SelectionSetNode) => { rowsets.set(rowKey(row), row); if (rowsets.size > 256) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query exceeds 256 row shapes"); for (const node of selections(set)) if (node.selectionSet) { if (node.name.value === "expand") discoverExpansion(row, row, node.selectionSet); else if (node.name.value === "filter" || node.name.value === "distinct") discoverRows(row, node.selectionSet); } }; const discoverObject = (row: QueryRow, set: SelectionSetNode) => { for (const node of selections(set)) if (node.selectionSet) { if (node.name.value === "_qx") { for (const meta of selections(node.selectionSet)) if (meta.name.value === "relations" && meta.selectionSet) for (const edge of selections(meta.selectionSet)) { const member = relations(row).find((m) => m.displayName === edge.name.value); if (member && many(member) && edge.selectionSet) discoverRows(relationRow(member), edge.selectionSet); } } else { const member = relations(row).find((m) => m.displayName === node.name.value); if (member) { if (!many(member)) discoverObject(objectRow(env.target(member)), node.selectionSet); else for (const entries of selections(node.selectionSet)) if (entries.name.value === "entries" && entries.selectionSet) for (const child of selections(entries.selectionSet)) if (child.name.value === "node" && child.selectionSet) discoverObject(objectRow(env.target(member)), child.selectionSet); } } } }; const discover = (root: InterfaceRevisionId) => { for (const operation of document?.definitions ?? []) if (operation.kind === "OperationDefinition") for (const node of selections(operation.selectionSet)) if (node.name.value === "root" && node.selectionSet) discoverObject(objectRow(root), node.selectionSet); }; const expand = (root: QueryRow, current: QueryRow, path: string[] = []): GraphQLObjectType => out( "Expand", current, () => { const result: GraphQLFieldConfigMap = {}; const paths = [...(expansionPaths.get(rowKey(root))?.values() ?? [])].filter((p) => path.every((part, i) => p[i] === part), ); for (const name of new Set(paths.map((p) => p[path.length]).filter((v): v is string => !!v))) { const member = relations(current).find((m) => m.displayName === name); if (member && many(member)) result[name] = { type: new GraphQLNonNull(rowset({ kind: "pair", source: root, target: relationRow(member) })), }; else { const field = fields(current).get(name); if (field?.kind === "row") result[name] = { type: new GraphQLNonNull(expand(root, field.row, [...path, name])) }; } } if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean }; return result; }, rowKey(root) + JSON.stringify(path), ); const rowset = (row: QueryRow): GraphQLObjectType => { const result = out("Rows", row, () => { const entry = out("GroupEntry", row, () => ({ key: { type: new GraphQLNonNull(GraphQLString) }, cursor: { type: env.cursor }, group: { type: new GraphQLNonNull(columns(row)) }, aggregate: { type: new GraphQLNonNull(aggregate(row)) }, })); const connection = out("Groups", row, () => ({ entries: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(entry))) }, pageInfo: { type: new GraphQLNonNull(env.pageInfo) }, })); roles.set(connection.name, { kind: "groups", row }); return { filter: { type: new GraphQLNonNull(rowset(row)), args: { where: { type: new GraphQLNonNull(where(row)) } } }, distinct: { type: new GraphQLNonNull(rowset(row)), args: { by: { type: new GraphQLNonNull(keys(row)) } } }, expand: { type: new GraphQLNonNull(expand(row, row)) }, aggregate: { type: new GraphQLNonNull(aggregate(row)), args: { where: { type: where(row) } } }, groups: { type: new GraphQLNonNull(connection), args: { by: { type: new GraphQLNonNull(keys(row)) }, where: { type: where(row) }, having: { type: having(row) }, orderBy: { type: new GraphQLList(new GraphQLNonNull(having(row, true))) }, first: { type: GraphQLInt }, all: { type: GraphQLInt }, after: { type: env.cursor }, }, }, }; }); roles.set(result.name, { kind: "rowset", row }); return result; }; const helpers = (id: InterfaceRevisionId): GraphQLObjectType | undefined => { const row = objectRow(id), edges = relations(row).filter(many); if (!edges.length) return undefined; const result = out("Relations", row, () => Object.fromEntries( edges.map((member) => [member.displayName, { type: new GraphQLNonNull(rowset(relationRow(member))) }]), ), ); roles.set(result.name, { kind: "relations", row }); return result; }; return { refs, fields, relations, relationRow, where, rowset, helpers, roles, discover, scalar, compare }; }