import type { OGMLogger } from '../execution/executor'; import type { PolicyContextBundle } from '../policy/types'; import { NodeDefinition, SchemaMetadata } from '../schema/types'; import { WhereCompiler } from './where.compiler'; export interface MutationResult { cypher: string; params: Record; } /** * Compiles create, update, delete, and label mutations into Cypher + params. */ export declare class MutationCompiler { private schema; /** * Cache of computed label strings per type name. * Bounded by the number of node types in the schema (~50-100 entries). */ private labelCache; private logger?; /** * WhereCompiler used ONLY to compile a relationship TARGET type's * `read` policy predicate for connect/disconnect target MATCHes, * mirroring the read paths (`WhereCompiler`/`SelectionCompiler`). * Injected by the OGM so it shares the user's compiler options; * lazily constructed with default options otherwise. */ private policyWhereCompiler?; constructor(schema: SchemaMetadata, options?: { logger?: OGMLogger; whereCompiler?: WhereCompiler; }); /** * Resolve and compile the TARGET type's `read` policy predicate for a * connect/disconnect target MATCH, mirroring the read path * (`WhereCompiler.buildTargetBundle` + `WhereCompiler.compile`). You * must be allowed to SEE a node to link/unlink it, so linking to an * unreadable node is the IDOR this closes; the codebase models the * row-level write gate for `update`/`delete` targets as `read` * ReadRestrictive policies (see policy/types.ts), so `read` is both * necessary and the semantically correct operation here. * * Returns `null` — leaving the emitted Cypher BYTE-IDENTICAL — when: * - no policy context is threaded (no policy bound / bypass active), OR * - no `paramCounter` is available to allocate policy params, OR * - the target type has no policy applicable to `read`, OR * - the resolved policy is `overridden` (compiles to nothing). * * When a target policy resolves, the compiled predicate is returned and * its bound params are merged into `params`. Policy params use the * shared `paramCounter` (`param`), continuing the sequence started by * the top-level WHERE, so they can never collide with the * connect/disconnect `where` params (prefix-named, e.g. * `connect__`) or the selection's params (allocated later * from the same counter). */ private buildTargetPolicyPredicate; private getPolicyWhereCompiler; /** * Compile a nested-write `where` (connect, disconnect, nested update, * nested/cascade delete) against `targetVar` through `WhereCompiler` — * the single where implementation (v2.3.0: the mutation compiler's * parallel builder was removed after repeatedly drifting from it). * Operators, null semantics, abstract relationship targets and — * crucially — traversal-policy enforcement are identical to a direct * `where`: a relationship filter inside it AND-stitches the traversed * type's `read` policy. The target's OWN operation policy is added * separately by `buildTargetPolicyPredicate`, so the root here is bound * as `NO_ROOT_POLICY`. * * Returns `''` for an absent or empty where. */ private compileTargetWhere; /** * The bulk-connect UNWIND fast path compiles each where key against the * ROW's own value (`connItem.where.node.`), which `WhereCompiler` * cannot express. It is therefore restricted to the one shape where * that is exact: every item's where is `{ node: { … } }` or bare * properties, holding only declared, stored scalar properties of the * target (registered operators allowed) with NON-NULL values — no * `AND`/`OR`/`NOT`/`node_NOT`, no relationship traversal, no `@cypher` * field. Every predicate it emits is then a scalar comparison against a * concrete row value, so it can only narrow matches, never fail open. * Anything else takes the per-item path through `WhereCompiler`. */ private isConnectFastPathEligible; /** * Surface a logical operator that contributed zero effective conditions. * Mirrors WhereCompiler.warnEmptyLogical — fires only on the anomalous * branch, so the hot path pays nothing. */ private warnEmptyLogical; /** Clear internal caches. Useful in tests to prevent cross-test pollution. */ clearCaches(): void; private getCachedLabelString; /** * Generate CREATE Cypher for one or more nodes. * Handles scalar properties, nested relationship creates, and connects. */ compileCreate(inputs: Record[], nodeDef: NodeDefinition, labels?: string[], /** * The caller's `create` bundle (v2.3.0). Its `resolveForType` gates * every connect target in the nested input by the TARGET type's `read` * policy — pre-2.3.0 the create path threaded no policy at all. */ policyContext?: PolicyContextBundle, /** * Shared `param` counter; continue it into the RETURN selection so * nested-filter params never collide with the selection's. */ paramCounter?: { count: number; }): MutationResult; /** * Generate UPDATE Cypher (SET properties + connect/disconnect). */ compileUpdate(_where: Record, update: Record | undefined, connect: Record | undefined, disconnect: Record | undefined, nodeDef: NodeDefinition, whereResult: { cypher: string; params: Record; preludes?: string[]; }, labels?: string[], returnMode?: 'node' | 'count', /** * The caller's resolved policy bundle for this mutation (the SOURCE * type's `update` bundle). Its `resolveForType` is used to resolve * each connect/disconnect TARGET type's `read` policy. `undefined` * when no policy is bound or a bypass is active — in which case the * emitted Cypher is byte-identical to before this enforcement. */ policyContext?: PolicyContextBundle, /** * Shared `param` counter, continued from the top-level WHERE * compile so target-policy params never collide with connect/ * disconnect `where` params or the selection's params. */ paramCounter?: { count: number; }): MutationResult; /** * Generate DELETE Cypher with optional cascade. * * v2.3.0 — each cascaded relationship honours its per-item `where` and * is gated by the TARGET type's `delete` policy (`buildNestedDelete`). * Pre-2.3.0 the cascade iterated only the relationship KEYS: the * documented `{ where }` was ignored, so every related node — shared * ones included — was deleted, with no policy check. */ compileDelete(nodeDef: NodeDefinition, whereResult: { cypher: string; params: Record; preludes?: string[]; }, deleteInput?: Record, /** The caller's `delete` bundle; gates each cascade target. */ policyContext?: PolicyContextBundle, /** Shared `param` counter, continued from the root WHERE. */ paramCounter?: { count: number; }): MutationResult; /** * Nested / cascade delete of ONE relationship's targets (v2.3.0), shared * by `compileDelete` (`Model.delete`'s `delete` input) and delete-inside- * `update`. One subquery per spec item: * * * CALL { * * MATCH (src)-[:REL]->(t) * WHERE AND * DETACH DELETE t * RETURN count(*) AS _del__ * } * * `spec` is one item or an array (singular vs list relationships); * `null` means "no cascade" (GraphQL InputMaybe). `{}` / no `where` * deletes every related node the target's `delete` policy permits. * A multi-level cascade (`delete` inside an item) and unknown item keys * are rejected instead of silently ignored. */ private buildNestedDelete; /** * Generate MERGE (upsert) Cypher with ON CREATE SET / ON MATCH SET. * Scalar properties only — nested relationship ops are not supported. */ compileMerge(where: Record, create: Record, update: Record, nodeDef: NodeDefinition, labels?: string[]): MutationResult; /** * Generate SET/REMOVE labels Cypher. */ compileSetLabels(nodeDef: NodeDefinition, whereResult: { cypher: string; params: Record; preludes?: string[]; }, addLabels?: string[], removeLabels?: string[]): MutationResult; /** * Generate batch CREATE (or MERGE for skipDuplicates) Cypher via UNWIND. * Scalar properties only — no nested relationship operations. * Returns count of created nodes. */ compileCreateMany(data: Record[], nodeDef: NodeDefinition, skipDuplicates?: boolean, labels?: string[]): MutationResult; /** * Resolve the PropertyDefinition of an edge (relationship) property so * Int/BigInt edge writes get the same Integer coercion as node writes. * Returns undefined when the relationship declares no properties type * (the coercion then passes the value through untouched). */ private getEdgePropDef; private buildCreateProperties; private buildCreateRelationships; /** * Process create/connect operations for a specific target node type. * Used by buildCreateRelationships for both union-member and non-union targets. */ private buildCreateRelationshipsForTarget; private buildConnects; private buildDisconnects; /** * Dispatch update operations for a union-typed relationship. * Each member key in the input is recursed into buildUpdateRelationships * with a narrowed relDef pointing at the concrete member type. */ private dispatchUnionUpdateOps; /** * Process nested relationship operations within an update input. * Handles: create, connect, disconnect, update, and arrays of these. */ private buildUpdateRelationships; /** * Operator suffixes supported in connect/disconnect WHERE conditions. * Maps suffix → Cypher operator template (use %v for variable, %p for param). */ private static readonly OPERATOR_SUFFIXES; /** * Parse a property key that may contain an operator suffix (e.g. `id_IN`, `name_CONTAINS`). * Returns the base property name and the Cypher expression template. */ private parseOperatorSuffix; private buildGeneratedIdClause; private extractConnectWhereConditions; private extractEdgeProperties; } /** * Map a nested-mutation connection where onto an equivalent NODE where on * the target, for `WhereCompiler` (v2.3.0). Mutation `where` never supports * edge filters, so the two shapes are interchangeable: * * { node: A } → A * { node_NOT: A } → { NOT: A } * { NOT: C } → { NOT: map(C) } * { AND: [C…] } → { AND: [map(C)…] } (likewise OR; `OR: []` * still matches nothing) * bare properties → themselves (legacy `{ id: 'x' }` shape) * * `edge`/`edge_NOT` throw (unsupported in mutations). In a connection- * shaped where, any other key throws too — the removed parallel builder * silently IGNORED such keys (`{ id: 'x', NOT: {…} }` dropped `id`), * widening the match. */ export declare function connectionWhereToNodeWhere(spec: Record): Record; //# sourceMappingURL=mutation.compiler.d.ts.map