import type { AggregateOptions, AnyBulkWriteOperation, BulkWriteOptions, BulkWriteResult, ChangeStreamDocument, ChangeStreamOptions, ClientSession, Collection, Document, Filter, FindOptions, InsertOneOptions, OptionalUnlessRequiredId, Sort, UpdateOptions } from 'mongodb'; import { ObjectId } from 'mongodb'; import { type CacheStore } from '@nl-framework/core'; import type { DocumentMetadata, BaseDocument, DocumentClass } from '../interfaces/document'; import { OrmRepository, type OrmEntityDocument } from '../interfaces/repository'; import type { WriteNotifier } from '../events/write-notifier'; /** Per-call read-through cache directive. */ export interface QueryCacheOptions { ttlMs?: number; /** Explicit cache key; defaults to a stable hash of the collection + query. */ key?: string; } export interface FindManyOptions extends FindOptions { withDeleted?: boolean; /** `@Ref`/`@RefArray` fields to populate (one level, batched — no N+1). */ populate?: Array> | string[]; /** Opt-in read-through cache for this call. Ignored inside a transaction. */ cache?: QueryCacheOptions; } /** Resolves the collection + metadata for a related document class on the same connection. */ export type RelationResolver = (target: DocumentClass) => Promise<{ collection: Collection; metadata: DocumentMetadata; } | null>; export interface RelationOptions { resolveRelation?: RelationResolver; logger?: { warn: (message: string) => void; }; /** Optional read-through cache backing `find(..., { cache })`. */ cache?: CacheStore; } export interface FindOneOptions extends FindManyOptions { } export interface WriteOptions { session?: ClientSession; } export interface ExistsOptions extends WriteOptions { withDeleted?: boolean; } export interface UpdateOneOptions extends WriteOptions { withDeleted?: boolean; } export interface DistinctOptions extends WriteOptions { withDeleted?: boolean; } export interface AggregateManyOptions extends AggregateOptions { withDeleted?: boolean; } /** Options accepted by keyset {@link MongoRepository.paginate}. */ export interface PaginateOptions { limit: number; cursor?: string; sort?: Sort; withDeleted?: boolean; session?: ClientSession; projection?: FindOptions['projection']; } /** One page of keyset pagination results. */ export interface PaginatedResult { items: TDoc[]; nextCursor: string | null; hasMore: boolean; } export interface BulkUpsertResult { matchedCount: number; modifiedCount: number; upsertedCount: number; } export declare class MongoRepository extends OrmRepository, FindManyOptions, FindOneOptions, OptionalUnlessRequiredId, Partial, OrmEntityDocument> { private readonly collection; private readonly metadata; private readonly notifier?; private readonly relationOptions; constructor(collection: Collection, metadata: DocumentMetadata, notifier?: WriteNotifier | undefined, relationOptions?: RelationOptions); private readonly cachedKeys; private emitWrite; get collectionName(): string; get entity(): DocumentClass; /** Human-readable entity name used in error messages (class name, falling back to collection). */ private get entityName(); private get props(); private get versionField(); /** * When the document is `@Document({ validate: true })`, run class-validator * against the entity before it reaches the driver. Throws the framework's * `ValidationException` (the same shape HTTP pipes produce). Undecorated fields * are skipped; `partial` skips missing properties for update/upsert paths. */ private hasHooks; /** Build a class instance from a plain object, preserving special-value identity. */ private hydrateInstance; /** Run the named lifecycle hooks sequentially on the given instance. A throw aborts. */ private runHooks; private validateWrite; /** * Times a read and reports its **filter shape** (never values) to any registered * `QueryObserver`. Zero overhead when none is registered (single boolean check). */ private observeRead; find(filter?: Filter, options?: FindManyOptions): Promise>>; private findInternal; private invalidateCache; findOne(filter?: Filter, options?: FindOneOptions): Promise | null>; private findOneInternal; findById(id: string | ObjectId, options?: FindOneOptions): Promise | null>; count(filter?: Filter, options?: FindManyOptions): Promise; /** Cheap existence check — soft-delete aware, stops at the first match. */ exists(filter?: Filter, options?: ExistsOptions): Promise; /** Like {@link findOne} but throws {@link EntityNotFoundException} instead of returning null. */ findOneOrFail(filter?: Filter, options?: FindOneOptions): Promise>; /** Like {@link findById} but throws {@link EntityNotFoundException} instead of returning null. */ findByIdOrFail(id: string | ObjectId, options?: FindOneOptions): Promise>; /** * Atomic single-document update. Returns the updated (post-image) document * mapped like {@link findOne}, or `null` when nothing matched. Soft-deleted * documents are excluded unless `withDeleted` is set. */ updateOne(filter: Filter, update: Partial, options?: UpdateOneOptions): Promise | null>; /** * When `@Version` is set and the update carries a version value, strip it from * `$set`, add a `$inc` on the version field, and return the current version so * the caller can pin it in the filter. Returns `undefined` when locking does * not apply. */ private applyOptimisticLock; private throwForFailedUpdate; /** * Atomically increment a numeric field on every matching document. Returns * the number of documents modified. `updatedAt` is bumped when the document * uses timestamps. */ increment(filter: Filter, field: Extract, by?: number, options?: WriteOptions): Promise; /** Distinct values for a field, soft-delete aware. */ distinct(field: Extract | string, filter?: Filter, options?: DistinctOptions): Promise; insertOne(doc: OptionalUnlessRequiredId, options?: InsertOneOptions): Promise>; insertMany(docs: OptionalUnlessRequiredId[], options?: BulkWriteOptions): Promise>>; save(entity: Partial & { id?: string; _id?: string | ObjectId; }, options?: WriteOptions): Promise>; updateMany(filter: Filter, update: Partial, options?: UpdateOptions): Promise; /** * When `@BeforeDelete`/`@AfterDelete` hooks are registered, load the matching * entities as hydrated instances so the hooks can run on them; otherwise skip * the extra read and return `null`. */ private loadForDeleteHooks; softDelete(filter: Filter, options?: WriteOptions): Promise; restore(filter: Filter, options?: WriteOptions): Promise; deleteHard(filter: Filter, options?: WriteOptions): Promise; /** Hard-delete every document matching the filter, regardless of soft-delete settings. */ deleteMany(filter: Filter, options?: WriteOptions): Promise; /** Raw bulk write against the underlying collection. Documents are passed through untouched. */ bulkWrite(operations: AnyBulkWriteOperation[], options?: BulkWriteOptions): Promise; /** * Upsert many documents in a single round trip, matched on the given * natural-key fields. Timestamps are handled like the other write paths: * `updatedAt` on every write, `createdAt` only on insert. */ bulkUpsert(docs: Array>, keyFields: Array>, options?: BulkWriteOptions): Promise; /** * Run an aggregation pipeline. When the document uses soft-delete and * `withDeleted` is not set, a not-deleted `$match` stage is prepended. User * `$match` stages are never rewritten. */ aggregate(pipeline: Document[], options?: AggregateManyOptions): Promise; /** * Streaming variant of {@link aggregate}. Yields raw pipeline output — results * are **not** mapped through `mapDocument` because the shape is pipeline-defined. * Don't hold the returned cursor open across a transaction boundary. */ aggregateCursor(pipeline: Document[], options?: AggregateManyOptions): AsyncIterable; private buildAggregateCursor; /** * Stream documents one at a time, each mapped through `mapDocument`, honouring * soft-delete filtering like {@link find}. Prefer this over {@link find} for * large result sets. Caveat: don't hold the cursor open across a transaction * boundary — the session may be committed/aborted before iteration finishes. */ findCursor(filter?: Filter, options?: FindManyOptions): AsyncIterable>; /** * Fetch a page of documents plus the total matching count, running the two * queries in parallel. The count ignores `skip`/`limit`. */ findAndCount(filter?: Filter, options?: FindManyOptions): Promise<[Array>, number]>; /** * Keyset (cursor) pagination — stable under concurrent writes and cheap at any * offset because it never uses `skip`. The cursor encodes the sort key values * of the last item plus the `_id` tiebreaker and the sort itself; supplying a * `cursor` produced under a different `sort` is rejected. Default sort `_id: 1`. */ paginate(filter: Filter | undefined, options: PaginateOptions): Promise>>; /** * Stream change events for this collection via a MongoDB change stream. Each * event's `fullDocument` (when present) is hydrated like {@link findOne}. * * **Requires a replica set** (change streams are unavailable on standalone * topologies). Don't hold the stream open across a transaction boundary. */ watch(pipeline?: Document[], options?: ChangeStreamOptions): AsyncIterable>; /** * Opt-in bridge that re-emits change-stream events into the {@link WriteNotifier} * so writes made outside this process surface to local listeners. Uses * `fullDocument: 'updateLookup'` so updates carry the post-image. Returns a * `stop()` to close the stream. Requires a replica set + a configured notifier. */ bridgeChangesToNotifier(options?: ChangeStreamOptions): { stop: () => Promise; }; private warnRelation; /** * Populate `@Ref`/`@RefArray` fields in place. Ids are collected across the * whole result set and fetched with a single `$in` query per relation (no * N+1). Missing targets become `null` (single) or are dropped (array), each * with a warning. Nested populate is out of scope. */ private populateDocuments; private queryRelated; private applyFilter; private prepareForInsert; private prepareForUpdate; private ensureIdentifiers; private normalizeIdValue; private mapDocument; /** Convert `@Embedded` subdocuments into instances of their declared class. */ private hydrateEmbedded; private buildIdFilter; } //# sourceMappingURL=mongo-repository.d.ts.map