import { DocumentNode } from 'graphql'; import { Driver } from 'neo4j-driver'; import { FulltextCompiler } from './compilers/fulltext.compiler'; import { MutationCompiler } from './compilers/mutation.compiler'; import { SelectNormalizer } from './compilers/select-normalizer'; import { SelectionCompiler } from './compilers/selection.compiler'; import { VectorCompiler } from './compilers/vector.compiler'; import { WhereCompiler } from './compilers/where.compiler'; import { ExecutionContext, OGMLogger } from './execution/executor'; import type { DetailedResolution, Operation, PolicyContext, PolicyDefaults, PolicyExplanation, ResolvedPolicies } from './policy/types'; import { NodeDefinition, SchemaMetadata } from './schema/types'; /** * Internal binding handed to `Model` by `OGM.withContext(ctx)`. Carries * the per-request ctx, a resolver function, defaults, and a logger * reference for `unsafe` bypass logging. * * NOT exported — created and consumed inside the OGM. */ export interface ModelPolicyBinding { ctx: PolicyContext; resolve: (typeName: string, op: Operation, ctx: PolicyContext) => ResolvedPolicies | null; /** * Full resolution (nothing dropped) for `explainPolicies`. Optional so * hand-built bindings stay valid; `OGMWithContext` always supplies it. */ resolveDetailed?: (typeName: string, op: Operation, ctx: PolicyContext) => DetailedResolution | null; defaults: PolicyDefaults; logger?: OGMLogger; /** Set when this binding belongs to a `unsafe.bypassPolicies()` OGM. */ globalBypass?: boolean; /** Stable version string for audit metadata. */ policySetVersion: string; } /** * Optional per-call escape hatch on every Model method's params bag. */ export interface UnsafeOptions { bypassPolicies?: boolean; } interface FindOptions> { limit?: number; offset?: number; sort?: TSort[]; } /** A single fulltext index query entry */ export interface FulltextIndexEntry { phrase: string; score?: number; } /** Relationship fulltext entry — index entries namespaced under a relationship field */ export type FulltextRelationshipEntry = Record; /** * Fulltext leaf: a single index query. * - Node index: `{ IndexName: { phrase, score? } }` * - Relationship index: `{ relFieldName: { IndexName: { phrase, score? } } }` */ export type FulltextLeaf = Record; /** Fulltext input with optional logical operators (OR/AND/NOT) */ export type FulltextInput = FulltextLeaf | { OR: FulltextInput[]; } | { AND: FulltextInput[]; } | { NOT: FulltextInput; }; /** Type guard: checks if a fulltext input is a leaf (not a logical operator) */ export declare function isFulltextLeaf(input: FulltextInput): input is FulltextLeaf; /** Type guard: checks if a value is a direct index entry (has `phrase`) */ export declare function isFulltextIndexEntry(value: FulltextIndexEntry | FulltextRelationshipEntry): value is FulltextIndexEntry; export interface MutationInfo { nodesCreated: number; nodesDeleted?: number; relationshipsCreated: number; relationshipsDeleted?: number; } export type MutationResponse = string extends K ? any : { info: MutationInfo; } & { [P in K]: T[]; }; /** * Public-facing interface for Model — used in generated XModel type aliases. * Excludes internal class properties (selectionSet setter, maxDepth, etc.) * so that jest.Mocked only requires CRUD methods. */ export interface ModelInterface = any, TWhere extends Record = any, TCreateInput extends Record = any, TUpdateInput extends Record = any, TConnectInput extends Record = any, TDisconnectInput extends Record = any, TDeleteInput extends Record = any, TPluralKey extends string = any, TMutationSelect extends Record = any, TSort = Record, TFulltext = FulltextInput> { find(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: FindOptions; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; create(params: { input: TCreateInput[]; labels?: string[]; selectionSet?: string | DocumentNode; select?: TMutationSelect; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; update(params: { where?: TWhere; update?: TUpdateInput; connect?: TConnectInput; disconnect?: TDisconnectInput; labels?: string[]; selectionSet?: string | DocumentNode; select?: TMutationSelect; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; delete(params: { where?: TWhere; delete?: TDeleteInput; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ nodesDeleted: number; relationshipsDeleted: number; }>; aggregate(params: { where?: TWhere; aggregate: { count?: boolean; [field: string]: boolean | undefined; }; labels?: string[]; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count?: number; [field: string]: unknown; }>; findFirst?(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: Omit, 'limit'>; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findUnique?(params: { where: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findFirstOrThrow?(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: Omit, 'limit'>; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findUniqueOrThrow?(params: { where: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; count?(params?: { where?: TWhere; labels?: string[]; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; upsert?(params: { where: TWhere; create: TCreateInput; update: TUpdateInput; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; createMany?(params: { data: TCreateInput[]; skipDuplicates?: boolean; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count: number; }>; updateMany?(params: { where?: TWhere; data: TUpdateInput; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count: number; }>; deleteMany?(params: { where?: TWhere; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count: number; }>; searchByVector?(params: { indexName: string; vector: number[]; k: number; where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; searchByPhrase?(params: { indexName: string; phrase: string; k: number; providerConfig?: Record; where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; } /** Compilers needed for read-only operations (find, aggregate, count). */ export interface QueryCompilers { where: WhereCompiler; selection: SelectionCompiler; fulltext: FulltextCompiler; } /** Additional compilers needed for write operations (create, update, delete). */ export interface MutationCompilers { selectNormalizer: SelectNormalizer; mutation: MutationCompiler; } /** * All compilers used by Model. * * `vector` is kept on `ModelCompilers` (not on `QueryCompilers`) because * vector search is a Model-only read-path concern and `InterfaceModel` * (which shares `QueryCompilers`) does not support it. Keeping it outside * `MutationCompilers` preserves that interface's "writes only" meaning. * Marked optional for backward compatibility with callers constructing * `ModelCompilers` literals before v1.3.0. */ export interface ModelCompilers extends QueryCompilers, MutationCompilers { vector?: VectorCompiler; } export declare class Model = any, TWhere extends Record = any, TCreateInput extends Record = any, TUpdateInput extends Record = any, TConnectInput extends Record = any, TDisconnectInput extends Record = any, TDeleteInput extends Record = any, TPluralKey extends string = any, TMutationSelect extends Record = any, TSort = Record, TFulltext = FulltextInput> implements ModelInterface { private nodeDef; private schema; private _selectionSet; private _parsedSelection; private _maxDepth; private _defaultSelection; /** * Global selection cache capped at 500 entries. No eviction — assumes bounded * selection set variety (fixed set of selectionSet strings used across resolvers). * In practice, NestJS services use a fixed set of selection strings, so growth is bounded. */ private static _selectionCache; /** Clear the static selection cache. Useful in tests to prevent cross-test pollution. */ static clearSelectionCache(): void; private whereCompiler; private selectionCompiler; private selectNormalizer; private mutationCompiler; private fulltextCompiler; private vectorCompiler; private executor; private policyBinding; private logger; constructor(nodeDef: NodeDefinition, schema: SchemaMetadata, driver: Driver, compilers?: ModelCompilers, logger?: OGMLogger, policyBinding?: ModelPolicyBinding); /** * Build a `PolicyContextBundle` for a single operation. Returns * `null` when no policies are bound (v1.6.0 path) or when the call * site requested `unsafe.bypassPolicies`. The latter case logs a * warning via the configured logger so the bypass is auditable. * * For default-deny `'throw'` mode, callers must throw BEFORE compile * — checked here and propagated by `assertNotDeniedAtCompile`. */ private resolvePolicyContext; /** * Throw `PolicyDeniedError` when default-deny is set to `'throw'` AND * the resolved policy set has no permissive that could match. This * path runs BEFORE compile so calls fail at the call site. */ private assertNotDeniedAtCompile; /** * Build `ExecutionContext` with audit metadata when policies are * configured. Preserves the user's transaction/session selection. */ private withAuditMetadata; /** Override the default RETURN clause -- legacy escape hatch */ set selectionSet(value: string | DocumentNode); set maxDepth(value: number); find(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: FindOptions; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; /** * Explain, per candidate node, the outcome of every `read` policy that * `find` would enforce for this model's bound context (GitHub issue #6). * * **This is a deliberate policy bypass.** Candidates are selected by * `where` / `labels` exactly as `find` selects them, but the root policy * predicate is REPORTED instead of applied — nodes the bound context * cannot see ARE returned, with `visible: false`. Use it only on * admin-only diagnostic paths. Every call logs a `warn` and is tagged * `explain: true` in transaction metadata, even with * `auditMetadata: false`. * * - `visible` is the exact enforcement verdict: the query projects the * same composed predicate `find` puts in its WHERE, and the verdict * recomputed from the per-policy outcomes must agree with it or the * call throws — it never returns a possibly-wrong explanation. * - Relationships in the selection are still filtered by their own * target-type policies, exactly as in `find`; only the root type's * policies are explained. * - When the executor opens its own session it is READ-mode, so the * server rejects any write. A caller-supplied `transaction` / * `session` is used as-is — the caller owns its access mode. * - `options.limit` defaults to 100 and may not exceed 1,000. */ explainPolicies(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: FindOptions; context?: ExecutionContext; }): Promise[]>; create(params: { input: TCreateInput[]; labels?: string[]; selectionSet?: string | DocumentNode; select?: TMutationSelect; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; update(params: { where?: TWhere; update?: TUpdateInput; connect?: TConnectInput; disconnect?: TDisconnectInput; labels?: string[]; selectionSet?: string | DocumentNode; select?: TMutationSelect; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; delete(params: { where?: TWhere; delete?: TDeleteInput; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ nodesDeleted: number; relationshipsDeleted: number; }>; aggregate(params: { where?: TWhere; aggregate: { count?: boolean; [field: string]: boolean | undefined; }; labels?: string[]; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count?: number; [field: string]: unknown; }>; setLabels(params: { where: TWhere; addLabels?: string[]; removeLabels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findFirst(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: Omit, 'limit'>; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findUnique(params: { where: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findFirstOrThrow(params?: { where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; options?: Omit, 'limit'>; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; findUniqueOrThrow(params: { where: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; count(params?: { where?: TWhere; labels?: string[]; fulltext?: TFulltext; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; upsert(params: { where: TWhere; create: TCreateInput; update: TUpdateInput; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise; createMany(params: { data: TCreateInput[]; skipDuplicates?: boolean; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count: number; }>; updateMany(params: { where?: TWhere; data: TUpdateInput; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count: number; }>; deleteMany(params: { where?: TWhere; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise<{ count: number; }>; /** * Run a top-k vector similarity search against a pre-computed embedding. * * The emitted Cypher binds the matched node to `n` via * `db.index.vector.queryNodes(...) YIELD node AS n, score`, then applies * the node's label (plus any additional `labels`) and the user-supplied * `where` as a post-filter. The selection is compiled through the same * pipeline as `find()`, so `select` / `selectionSet` semantics match. * * **Requirements** * - Neo4j **5.11+** (for `db.index.vector.queryNodes`). * - A vector index must be created out-of-band via * `CREATE VECTOR INDEX ... FOR (n:Label) ON n.embedding OPTIONS { ... }`. * grafeo-ogm does not create vector indexes automatically. * * **`k` clamping** — `k` is silently clamped to the range `[1, 1000]` by * the compiler to prevent unbounded result sets. Requests for `k > 1000` * will return at most 1000 results without a runtime warning. * * @returns Array of `{ node, score }` pairs ordered as returned by the * Neo4j vector index (most similar first). */ searchByVector(params: { indexName: string; vector: number[]; k: number; where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; /** * Run a top-k vector similarity search keyed on a text phrase. Requires * the matching `@vector` index to declare a `provider` so Neo4j's * `genai.vector.encode` can produce the embedding server-side. * * `providerConfig` (API tokens, model overrides, etc.) is passed as a * Cypher parameter, never interpolated into the query string. * * **Requirements** * - Neo4j **5.11+** with the **GenAI plugin** installed * (`genai.vector.encode` is shipped by the plugin, not core). * - The matching `@vector` index in your schema must set `provider` (e.g. * `"OpenAI"`, `"VertexAI"`). Without it, `searchByPhrase` throws an * `OGMError` at compile time. * - A vector index must exist in the database (create it manually via * `CREATE VECTOR INDEX ...`). * * **`k` clamping** — same as `searchByVector`: silently clamped to * `[1, 1000]` to prevent unbounded result sets. */ searchByPhrase(params: { indexName: string; phrase: string; k: number; providerConfig?: Record; where?: TWhere; selectionSet?: string | DocumentNode; select?: TSelect; labels?: string[]; context?: ExecutionContext; unsafe?: UnsafeOptions; }): Promise>; /** * Shared pipeline for `searchByVector` / `searchByPhrase`. Handles the * selection resolution, label filter, user WHERE composition, projection * compilation, and record mapping. The CALL prelude is supplied by the * caller so that the vector vs. phrase branches share everything else. */ private runVectorSearch; /** * Parse a selectionSet string (or DocumentNode) with LRU caching. Extracted * from the four previously-duplicated resolve-selection sites so one cache * dance lives in one place. Cache is capped at 500 entries (same as before). */ private parseSelectionSetCached; /** * Resolve a `SelectionNode[]` from `select` / `selectionSet` / the * instance-level selectionSet / defaults — matching `find()`'s behavior. */ private resolveSelection; /** * Root `'create'` checks (default-deny + WITH CHECK) — shared with every * nested create via `src/policy/nested-writes.ts`. */ private evaluateCreatePolicies; /** * Root WITH CHECK for create/update input — shared with every nested * write via `src/policy/nested-writes.ts`. */ private evaluateWriteRestrictives; private defaultSelection; /** * Replace the plain "RETURN n" in mutation cypher with a projected RETURN * when a selectionSet is provided, enabling relationship traversals. * * If the projection references `@cypher` scalar fields, the corresponding * CALL preludes are stitched immediately before the new RETURN. */ private applySelectionSetToMutation; /** * Apply a type-safe `select` object to a mutation's RETURN clause. * Only projects entity fields when select[pluralName] is present. */ private applySelectToMutation; /** * Build a narrowed result object based on which keys are present in `select`. */ private buildSelectResult; /** * Replace "RETURN n" in upsert cypher with a projected RETURN clause. */ private applySelectionSetToUpsert; /** * Apply a type-safe `select` object to an upsert's RETURN clause. */ private applySelectToUpsert; private compileOptions; } export {}; //# sourceMappingURL=model.d.ts.map