import type { OGMLogger } from '../execution/executor'; import type { Operation, PermissivePolicy, PolicyContextBundle, RestrictivePolicy } from '../policy/types'; import { NodeDefinition, SchemaMetadata } from '../schema/types'; import { CypherFieldScope } from '../utils/cypher-field-projection'; export interface WhereResult { cypher: string; params: Record; /** * Pre-WHERE lines (`CALL { ... }` + `WITH ...` pairs) needed to resolve * any `@cypher` scalar fields referenced at the TOP level of this where * input. The caller MUST emit these between the MATCH and the WHERE * clause; otherwise the compiled body references aliases that are not * in scope and Neo4j will fail. * * Preludes for nested scopes (e.g. `r0` inside a `_SOME` quantifier) * are stitched directly into the EXISTS body inside `cypher` — the * caller never has to handle those. */ preludes?: string[]; } /** * One resolved policy's compiled contribution to the policy clause. * `parts` are its boolean fragments in emission order (the `when` part, * then the `cypher` part) — exactly what `composePolicyClause` joins. * Empty `parts` → the policy emitted no predicate: it abstained, or (for * restrictives) it is write-side or gated off by `appliesWhen`. */ export interface CompiledPolicy { readonly policy: PermissivePolicy | RestrictivePolicy; readonly parts: ReadonlyArray; } /** * Per-policy compilation of one (typeName, op) frame. Positionally * aligned with the bundle's `resolved.permissives` / * `resolved.restrictives` — one entry per policy, in order. */ export interface CompiledPolicyFragments { readonly permissives: ReadonlyArray; readonly restrictives: ReadonlyArray; } /** Result of `WhereCompiler.compileForExplain` (explain path). */ export interface ExplainWhereResult { /** User where body only — the candidate filter. `''` when absent. */ userCypher: string; /** User-where params followed by policy params (same as `compile()`). */ params: Record; /** Top-level `@cypher` preludes for BOTH the user where and policies. */ preludes: string[]; /** * Root policy clause, or `null` when none applies (override fired). * `clause` is byte-identical to what `compile()` AND-stitches. */ policy: { fragments: CompiledPolicyFragments; clause: string; } | null; } type OperatorSuffix = string; export interface WhereCompilerOptions { /** Set of operator suffixes to reject at runtime (e.g. `new Set(['_MATCHES'])`) */ disabledOperators?: Set; /** * When `true`, the compiler throws `OGMError` if a `where` clause * references a field name that is not declared on the target type. * Default: `false` — preserves pre-1.7.5 behaviour where typo'd * field names compiled to `n. = $param` and silently produced * empty results. Opt in via `OGMConfig.features.strictWhere = true`. */ strictWhere?: boolean; /** * Logger used to surface a `warn` when a logical operator (`AND`/`OR`) * compiles with zero effective conditions — almost always a * dynamically-built filter that received an empty list. The OGM * passes its `config.logger` here. */ logger?: OGMLogger; } /** * Compiles a Where input object into a Cypher WHERE clause fragment + params. */ export declare class WhereCompiler { private schema; private disabledOperators; private strictWhere; private logger?; constructor(schema: SchemaMetadata, options?: WhereCompilerOptions); /** * Surface a logical operator that contributed zero effective conditions. * Fires only on the anomalous branch, so the hot path pays nothing. */ private warnEmptyLogical; compile(where: Record | undefined | null, nodeVar: string, nodeDef: NodeDefinition, paramCounter?: { count: number; }, options?: { /** * Vars already in the surrounding pipeline that every emitted `WITH` * must carry forward (e.g. `score` for vector search, `__typename` * for `InterfaceModel`). Without this, the WITH inside the prelude * would drop those vars and downstream RETURN/ORDER BY breaks. */ preserveVars?: ReadonlyArray; /** * Policy context for this query. When present, the resolved * permissive/restrictive set is AND-stitched into the compiled * body sharing the same `paramCounter` and prelude scope. * * If `resolved.overridden` is true, this is a no-op (byte- * identical to no-policy emission). When `resolved` is empty * AND `defaults.onDeny === 'empty'`, the policy clause becomes * `false` (default-deny). When `'throw'`, the call site is * responsible for rejecting BEFORE compile — see `Model`. */ policyContext?: PolicyContextBundle; /** * Caller-owned `@cypher` prelude scope (v2.3.0). When given, preludes * register into it and are NOT returned — the caller emits the scope * once, so several compiles against the same variable (a connection's * node filter and its target policy) share one `CALL`/`WITH` chain. * `preserveVars` is ignored (the scope already carries its own). */ scope?: CypherFieldScope; }): WhereResult; /** * Explain-path twin of `compile()`, used by `Model.explainPolicies`. * Compiles the user `where` EXACTLY as `compile()` does — same order, * same `paramCounter`, same `@cypher` prelude scope, same policy-aware * relationship traversal — but returns the root policy clause * separately instead of AND-stitching it: as per-policy fragments AND * as the composed string `compile()` would have stitched. Both come * from one compilation, so an explanation cannot drift from * enforcement. * * `policy` is `null` when no root policy clause applies (an override * fired). * * @internal */ compileForExplain(where: Record | undefined | null, nodeVar: string, nodeDef: NodeDefinition, paramCounter: { count: number; }, policyContext: PolicyContextBundle): ExplainWhereResult; /** * The policy clause guarding a node of a possibly ABSTRACT type for one * operation (v2.3.0) — the single construction behind every target- * policy consumer (nested selection, traversal filters, nested writes) * and `InterfaceModel`'s root clause: * * - concrete type → its own composed clause for `op` (byte-identical * to compiling it directly); `null` when it has no policy for `op` * or an override fires. * - interface / union → * `(CASE WHEN v:M1 THEN WHEN v:M2 THEN … ELSE false END)` * over the concrete members, each branch being exactly that member's * own clause (`resolveForType(M, op)` already folds in M's interface * policies). A member without a policy for `op`, or whose override * fires, is `true`; when EVERY member is `true` the result is `null` * (unconstrained, like a concrete type without policies). `ELSE false` * excludes a node carrying no known member label (defense in depth). * * Pre-2.3.0 abstract targets resolved the ABSTRACT type's name, so the * implementers' own policies were never applied through relationships. * * `fallbackOp`: resolve it for a type with NO policy for `op` — the * `aggregate` → `read` fallback `Model.aggregate` applies at the root. * * `preludes` (for `@cypher` fields referenced by a policy) are returned * to the caller; nested contexts reject them. * * @internal */ compileTargetPolicyClause(typeName: string, varName: string, op: Operation, policyContext: PolicyContextBundle, paramCounter: { count: number; }, fallbackOp?: Operation): { cypher: string | null; params: Record; preludes: string[]; }; private compileConcreteTargetClause; /** Concrete members of an interface / union; `null` for any other type. */ private abstractMembers; /** * Shared body of `compile()` / `compileForExplain()`: compiles the user * where, then the policy fragments, sharing one `paramCounter` and one * top-level `@cypher` scope. Returns `null` when there is nothing to * compile (no user where and no active policy). */ private compileUserAndPolicy; /** * Compile each resolved policy of a single (typeName, op) frame into * its boolean fragments ("parts"), WITHOUT combining them — * `composePolicyClause` does that. Shares the same `paramCounter` and * `scope` as the user where so that nothing collides downstream. * * The result is positionally aligned with `resolved.permissives` / * `resolved.restrictives`: one entry per policy, in order, even when a * policy contributes no part. * * Permissive `cypher.params` keys are namespaced with `policy_p_` * to guarantee no collision with `param0..N`. */ private compilePolicyFragments; /** * Build a `PolicyContextBundle` for a target type when crossing a * node-type boundary inside a relationship traversal (`_SOME` / * `_NONE` / `_ALL` / `_SINGLE` / connection-where `node`). Mirrors the * canonical pattern at `selection.compiler.ts:826-847`. * * Returns `undefined` when: * - the input `policyContext` is `undefined` (caller has no policy state), OR * - `resolveForType(typeName, 'read')` returns `null` (no policy * registered for the target type). * * The synthesized bundle reuses the same `resolveForType` so further * nesting (target → target's relationship → ...) keeps cascading. */ private buildTargetBundle; /** * Compile a traversal's filter against its target node with the * caller's filter (`user`) and the target's `read` policy (`policy`) * kept SEPARATE, so each quantifier composes them correctly — the * policy must never sit inside a negation (v2.3.0). Pre-2.3.0 the policy * was compiled INTO the filter, so `_ALL` (`NOT (user AND policy)`) was * falsified by hidden related nodes (an existence oracle), and a * connection `node_NOT` was SATISFIED by them. * * - no policy context → plain compile. * - concrete target → one `compileUserAndPolicy` pass (shared `@cypher` * prelude scope); the bundle cascades as `NO_ROOT_POLICY` at any depth. * - interface target → filter via a `NO_ROOT_POLICY` traversal bundle, * policy via `compileTargetPolicyClause` (CASE over the members' * own clauses — pre-2.3.0 only the interface's own policies ran). * * `''` for an absent part. With `scope`, concrete-target preludes * register into it (caller emits); otherwise they are returned. * Shared with `SelectionCompiler`'s nested-projection filters. * * @internal */ compileTargetBody(where: Record | undefined | null, targetVar: string, targetDef: NodeDefinition, counter: { count: number; }, policyContext: PolicyContextBundle | undefined, scope?: CypherFieldScope): { user: string; policy: string; params: Record; preludes: string[]; }; private compileConditions; /** * Resolve the Cypher reference for a where-clause field. For stored * properties this is `.`. For `@cypher` scalar fields, * the field is registered in the scope (creating a CALL prelude on the * first reference) and the alias is returned. */ private resolveFieldRef; private tryCompileConnection; /** * Compile a connection-where-input — the value at * `where.Connection*: { ... }`. Recognises: * - `node` / `node_NOT` — target node Where filter (negation wraps in `NOT (...)`) * - `edge` / `edge_NOT` — edge property Where filter (only when relationship has properties) * - `AND` / `OR` — array of nested connection-where-inputs joined with the operator * - `NOT` — single nested connection-where-input wrapped in `NOT (...)` * * All nested clauses live inside the SAME EXISTS body — i.e. they * constrain the same `(relVar, edgeVar)` pair. This matches the codegen * shape declared in `connection-emitter.ts`. */ private compileConnectionWhereInput; private tryCompileRelationship; /** * Compiles a relationship WHERE clause targeting a union type. * Union WHERE inputs use member names as keys (e.g., `{ StandardDose: {} }`). * Each member generates a separate EXISTS pattern using the member's labels. * Multiple members are combined with OR. */ private compileUnionRelationship; /** * Compile `: null` on a node (v2.3.0). The operator suffix is split * off FIRST: pre-2.3.0 the whole key was treated as a property name, so * the common "is not null" idiom `{ deletedAt_NOT: null }` compiled to * `n.deletedAt_NOT IS NULL` — always true, silently matching every row * (and silently restricting nothing when used in a restrictive policy). * * field: null → field IS NULL * field_NOT: null → field IS NOT NULL * rel: null → NOT EXISTS { MATCH (n)-[:REL]->(…) } * rel_NOT: null → EXISTS { MATCH (n)-[:REL]->(…) } * any other operator → OGMError (null has no meaning there) * undeclared field → OGMError regardless of `strictWhere` — a null * filter on an undeclared field matches every * row (fail OPEN), unlike a non-null one * * A field literally named `key` always wins over suffix parsing. */ private compileNullCondition; /** * Scalar half of the null semantics above, shared by node properties and * relationship-edge properties: `IS NULL` / `IS NOT NULL`, or an * `OGMError` for any other operator and for undeclared fields. */ private compileNullScalar; /** * `NOT EXISTS` (no related node) / `EXISTS` (at least one) for a * relationship null filter. Byte-identical to the pre-2.3.0 `rel: null` * emission for the `NOT EXISTS` case. */ private compileRelationshipExistence; private compileEdgeConditions; /** * Compile a scalar where-condition. If `fieldName` resolves to a * `@cypher` scalar property and `scope` is provided, the field is * registered in the scope (producing a CALL prelude on first use) and * the alias is used in the predicate. Otherwise the predicate is * compiled against `.` as before. */ private compileScalarCondition; } /** * Stitch a user where-body and a policy clause into a single Cypher * fragment. Both come from `compileConditions` so each is already a * valid boolean expression. Empty user bodies skip the AND wrap. */ export declare function stitchUserAndPolicy(userBody: string, policyClause: string): string; /** * The bundle a traversal's USER filter compiles under (v2.3.0): no clause * for the target itself (its policy is composed separately by the caller), * but `resolveForType` stays live so deeper traversals keep enforcing. * * @internal */ export declare function traversalBundle(policyContext: PolicyContextBundle): PolicyContextBundle; /** * The boolean value ONE compiled policy contributes to * `composePolicyClause`, as a standalone expression (explain path): a * permissive's parts are OR-ed — they join the permissive disjunction — * and a restrictive's parts are AND-ed — they join the restrictive * conjunction. OR/AND are associative in Cypher's three-valued logic, * so grouping per policy preserves the composed clause's truth value. * * Returns `null` when the policy has no parts (it abstained). */ export declare function policyValueExpression(kind: 'permissive' | 'restrictive', parts: ReadonlyArray): string | null; export {}; //# sourceMappingURL=where.compiler.d.ts.map