import type { PolicyContextBundle } from '../policy/types'; import type { NodeDefinition, SchemaMetadata } from '../schema/types'; import { CypherFieldScope } from '../utils/cypher-field-projection'; import { type WhereCompiler } from './where.compiler'; /** * Represents a single node in the parsed selection tree. */ export interface SelectionNode { fieldName: string; alias?: string; isScalar: boolean; isRelationship: boolean; isConnection: boolean; children?: SelectionNode[]; /** WHERE filter for relationship pattern comprehensions (Prisma-like select) */ relationshipWhere?: Record; connectionWhere?: Record; edgeChildren?: SelectionNode[]; /** Sort spec for array relationship pattern comprehensions (Prisma-like select) */ orderBy?: Array<{ field: string; direction: 'ASC' | 'DESC'; }>; /** * Sort spec for connection edges. Each entry targets either the related node * scalars (`scope: 'node'`) or @relationshipProperties scalars (`scope: 'edge'`). */ connectionOrderBy?: Array<{ field: string; direction: 'ASC' | 'DESC'; scope: 'node' | 'edge'; }>; } /** * Compiles a selection set (parsed SelectionNode[]) into a Cypher RETURN map projection. * * Handles scalar fields, relationship pattern comprehensions (array and singular), * connection fields with edge properties, and nested traversals with depth limiting. */ export declare class SelectionCompiler { private schema; private static readonly DEFAULT_MAX_DEPTH; /** Maximum number of distinct parsed selection sets retained. */ private static readonly PARSE_CACHE_MAX_ENTRIES; /** * Real LRU cache of parsed selection sets, capped at * `PARSE_CACHE_MAX_ENTRIES`. Pre-1.7.5 the implementation was * `if (size < 200) set(...)` — once saturated, every miss had to * re-parse via `graphql.parse()` because nothing was ever inserted * (and nothing was ever evicted to make room). Apps with high * selection-set cardinality silently paid full parse cost on every * request post-saturation. The new policy: on hit, delete + re-set * (so the entry moves to the most-recently-used end of the Map's * insertion-order iteration); on miss, evict the oldest entry * (`Map.keys().next().value` — Maps preserve insertion order) before * inserting the new one. */ private parseCache; private whereCompiler?; constructor(schema: SchemaMetadata, whereCompiler?: WhereCompiler); /** Clear the parse cache. Useful in tests to prevent cross-test pollution. */ clearCache(): void; /** * Compile selection nodes into a Cypher map projection string. * * @param selection - Parsed selection nodes * @param nodeVar - Cypher variable for the matched node (e.g., 'n') * @param nodeDef - Node definition from schema metadata * @param maxDepth - Maximum traversal depth (default 5) * @param currentDepth - Current depth (used internally for recursion) * @returns Cypher map projection string, e.g. "n { .id, .name, drugs: [...] }" */ compile(selection: SelectionNode[], nodeVar: string, nodeDef: NodeDefinition, maxDepth?: number, currentDepth?: number, params?: Record, paramCounter?: { count: number; }, /** * Scope for `@cypher` scalar field projections at THIS pipeline level. * When provided, `@cypher` scalar fields in `selection` are registered * here so the caller can emit the CALL preludes before the RETURN * (preferred path: dedupes references and runs once per row binding). * When `null` (or omitted), `@cypher` scalar fields fall back to an * inline `head(COLLECT { WITH AS this })` projection — * required inside nested relationship pattern comprehensions where * CALL { ... } preludes have no anchor, and tolerated at the top level * for ad-hoc compile() callers that don't supply a scope. */ cypherScope?: CypherFieldScope | null, /** * Policy context for nested-selection enforcement. When set, every * relationship pattern comprehension, connection edge, and union * branch resolves the target type's `'read'` policy via * `policyContext.resolveForType` and AND-stitches it into the inner * WHERE clause. Without this, traversals would bypass policies. */ policyContext?: PolicyContextBundle | null): string; /** * Parse a GraphQL selection set string into SelectionNode[]. * * @param selectionSet - Raw string like "\{ id name drugs \{ id \} \}" * @returns Parsed SelectionNode array */ parseSelectionSet(selectionSet: string): SelectionNode[]; /** * Convert a GraphQL SelectionSetNode into SelectionNode[]. */ private convertSelectionSet; /** * Parse the children of a connection field, extracting node and edge children. */ private parseConnectionChildren; /** * Compile a relationship field into a Cypher pattern comprehension. */ private compileRelationship; /** * Compile a relationship field within a union-target context. * * Different union members can have the same field name (e.g., "populations") * but different underlying Neo4j relationship types. This generates a * CASE WHEN expression that checks the node's labels and uses the correct * relationship type for each member. * * Example output: * CASE * WHEN n0:FormPresentationIcon THEN [(n0)-[:FP_POP]->(p:Population) | p { .id }] * WHEN n0:AdministrationRateIcon THEN [(n0)-[:AR_POP]->(p:Population) | p { .id }] * ELSE [] * END */ private compileUnionRelationshipField; /** * Compile a connection field into a Cypher pattern comprehension with edge properties. */ private compileConnection; /** * AND-combine an optional user where (`select.where` / `connectionWhere`) * with the target type's `'read'` policy and emit a `WHERE …` fragment * (with leading space). Returns `''` when nothing applies. * * The returned where lives inside a pattern comprehension so any * `@cypher` field used by the policy MUST throw — the comprehension * cannot host CALL { ... } subqueries. The error message points to the * documented limitation. */ private compileNestedWhere; /** * Compile a connection's `edge`-side WHERE filter against the relationship * properties type. Reuses `WhereCompiler.compile()` by synthesizing a * `NodeDefinition` from the `RelationshipPropertiesDefinition` — full * operator support (scalar ops, AND/OR/NOT, `mode`) flows through for free. * * Edges have no policy enforcement (policies bind to node typeNames), so * this never resolves a policy context. `@cypher` field projections are * rejected with the same error pattern as nested-node selection where: * pattern comprehensions cannot host CALL { ... } subqueries. */ private compileEdgeWhere; /** * Compile a __typename expression for a node. * For union targets: picks the label matching a union member name. * For concrete types: returns a string constant. */ private compileTypename; /** * Compile a simple where object into a Cypher WHERE clause fragment. * This is a minimal implementation for connection where filters. */ private compileSimpleWhere; } //# sourceMappingURL=selection.compiler.d.ts.map