import { DocumentData, Precondition, Timestamp } from 'firebase-admin/firestore'; import { Status } from 'google-gax'; import { StructuralDatabase } from '../structural-database.js'; import { DocumentFieldValue } from './internal-types.js'; import { PathData, PathDataProvider, PathType } from './path.js'; interface OperationStats { /** * Total number of transactional document write operations (excluding deletes) that resulted * in a change to stored data. * * Counting rules: * - Increment once per document write that modifies data (create or update), whether partial * or complete. * - Excludes writes that produce no effective change (tracked under `noopWrites`). * - Every write is treated as transactional for accounting purposes, regardless of whether it * occurred within an explicit `Transaction` context. */ readonly writes: number; /** * Total number of document read operations that returned an existing document. * * Counting rules: * - Increment once per document delivered by a read operation (e.g., `get`, query, or listen * snapshot) when the document exists. * - Excludes read attempts that return no data (tracked under `noopReads`). */ readonly reads: number; /** * Total number of document delete operations that successfully removed existing documents. * * Counting rules: * - Increment once per document that was actually deleted. * - Excludes delete attempts that targeted non-existent documents (tracked under `noopDeletes`). */ readonly deletes: number; /** * Total number of document read operations that produced no results because the * target documents did not exist or the query result set was empty. * * Counting rules: * - **Direct lookups:** One count per document reference read that does not exist * (e.g. `DocumentReference.get()`, `IBatchGetDocumentsRequest`, or an * `IListenRequest` targeting specific documents). * - **Queries with no matches:** One count for a query execution that returns * zero documents (e.g. `IListDocumentsRequest`, `IRunQueryRequest`, * `IRunAggregationQueryRequest`, or an `IListenRequest` specifying a query). * * Note: This metric tracks *no-op* read attempts only; successful reads of existing * documents are accounted for under `reads`. */ readonly noopReads: number; /** * Total number of transactional document write operations (excluding deletes) that resulted * in no effective change to stored data. * * Counting rules: * - Increment once per attempted document write (partial or complete) whose final stored state * is identical to the prior state. * - Tracked for fidelity with Firestore semantics, which may still bill for such no-op writes. */ readonly noopWrites: number; /** * Total number of transactional document delete operations that targeted non-existent documents. * * Counting rules: * - Increment once per attempted delete that had no effect because the document did not exist. * - Tracked for fidelity with Firestore semantics, which may still bill for such no-op deletes. */ readonly noopDeletes: number; } interface StructuralStats { /** * Total number of active (materialized) documents that currently have stored data. * Does not include placeholder documents. */ readonly documentCount: number; /** * Total number of active collections that currently contain one or more documents. * Does not include placeholder collections. */ readonly collectionCount: number; /** * Total number of placeholder document nodes (structural documents with no stored data). * These are created to maintain tree structure (e.g., a document node that exists only * to anchor subcollections). */ readonly stubDocumentCount: number; /** * Total number of placeholder collection nodes (structural collections with zero documents). * These exist only to maintain tree structure (e.g., referenced subcollection paths that * currently contain no documents). */ readonly stubCollectionCount: number; } /** * Statistical counters for database activity and structural state maintained by the * in-memory Firestore mock. * * This interface exposes metrics that track: * - **Structural state:** The number of active vs. structural (placeholder) documents * and collections currently present in the database tree. * - **Operations:** Totals for writes, reads, and deletes, along with their no-op * counterparts (operations that produced no effective change or returned no data). * * All counters are cumulative within the lifetime of the current database context (or until * `reset()` is invoked). * They are primarily intended for validation in tests and for fidelity with Firestore * semantics (e.g. Firestore may still bill for no-op operations). */ export type DatabaseStats = OperationStats & StructuralStats; /** * Context passed to a {@link FieldTransformFunction} when resolving a field * transform during a write (e.g., `set`, `update`, `commit`, BulkWriter, or * within a transaction). * * @remarks * - Values are **read-only** and stable for the duration of the write * operation. * - `serverTime` is the canonical server clock chosen for the write; all * transforms in the same write/batch share this exact timestamp to mirror * Firestore semantics. * - `fieldValue` is the field’s current value **before** the transform is * applied (i.e., after the base document data is considered but prior to * transform resolution). It is `undefined` if the field does not exist. */ export interface FieldTransformContext { /** * The canonical server timestamp for this write. All transforms in the same * commit/batch observe this same value. */ readonly serverTime: Timestamp; /** * The existing value at the target field path prior to applying the transform, * or `undefined` if the field does not exist. */ readonly fieldValue?: DocumentFieldValue; } /** * A function that computes the new value for a field during transform * resolution. * * @param context Context for the current write, including the canonical * `serverTime` and the field’s pre-transform value (if any). * @returns The value to be written to the field. * * @remarks * - Implementations should be **pure** and **synchronous**. * - Throwing will cause the write to fail with the surfaced error. * - The returned value must be a valid {@link DocumentFieldValue}. * * @example * // Server timestamp transform * const toServerTimestamp: FieldTransformFunction = ({ serverTime }) => serverTime; * * @example * // Increment-by-N transform * const incrementBy = (n: number): FieldTransformFunction => * ({ fieldValue }) => (typeof fieldValue === 'number' ? fieldValue + n : n); */ export type FieldTransformFunction = (context: FieldTransformContext) => DocumentFieldValue; /** * Describes a single field transform to be applied atomically with a write. * * @remarks * - `path` may be a dot-delimited string (e.g., `"stats.count"`) or an array of * field path segments (e.g., `["stats", "count"]`). * - The `transformer` is evaluated at commit time using a stable * {@link FieldTransformContext}. Its result replaces the value at `path`. * - Multiple transforms in the same write observe the same `serverTime`. */ export interface DocumentFieldTransform { /** * Target field path to transform. Accepts a dot-delimited string or an array * of path segments. */ path: string | string[]; /** * Function that computes the new field value during transform resolution. */ transformer: FieldTransformFunction; } /** * Controls how new data is applied to an existing Firestore document. * * This type unifies the semantics of `set()` and `update()` operations: * * - `'root'` * Replace the entire document with the provided data. * Equivalent to `set(data)` or `set(data, { merge: false })`. * Unspecified fields are removed. * * - `'branch'` * Deep-merge provided map fields into the existing document. * Equivalent to `set(data, { merge: true })`. * Nested maps are merged recursively; scalars and arrays replace. * Unspecified branches remain unchanged. * * - `'node'` * Apply only the explicitly specified field paths, without recursive merge. * Equivalent to `set(data, { mergeFields: [...] })` or `update(data)`. * Each path sets or deletes its target field; sibling fields are unaffected. */ export type MergeGranularity = 'root' | 'branch' | 'node'; /** * A normalized **set** (create/overwrite/merge) operation. * Produced by decoding a GAPIC `IWrite` into the mock’s internal representation. * * Notes: * - `merge` encodes the set granularity (e.g., full overwrite, merge all, merge specific fields), * already resolved from any API-level input (e.g., `set(..., { merge: true|fields })`). * - `precondition`, when present, must be satisfied at apply-time (e.g., `exists`, `updateTime`). */ export interface NormalizedSet { /** Discriminator: identifies this operation as a set. */ type: 'set'; /** Fully-qualified document path (e.g., `"cities/SF"`). */ path: string; /** Document payload to write after server-side transforms are evaluated. */ data: DocumentData; /** * Merge granularity computed from the original API call. * Determines whether the set is a full overwrite or a partial field merge. */ merge: MergeGranularity; /** * Optional per-field transforms to evaluate atomically with this set. * * @remarks * - Each entry targets a field `path` and supplies a pure `transformer` * evaluated **after** merge granularity is resolved, using a stable * {@link FieldTransformContext} (shared `serverTime`, pre-transform view). * - The transform’s result replaces the value at `path` in the final document. * - All transforms in the same write share the same `serverTime` and are * applied atomically. Duplicate target paths are invalid. * * @example * // Apply server timestamp and increment atomically * transformers: [ * { path: 'updatedAt', transformer: ({ serverTime }) => serverTime }, * { path: 'metrics.count', transformer: ({ fieldValue }) => * typeof fieldValue === 'number' ? fieldValue + 1 : 1 * } * ] */ transformers?: DocumentFieldTransform[]; /** * Exact field paths targeted by a field-mask write. * * This is required to distinguish replacing a nested node from recursively * merging the maps used to reach that node. */ mergePaths?: string[][]; /** * Optional write precondition (e.g., exists/doesNotExist, updateTime match). * If not met at apply-time, the write fails with a precondition error. */ precondition?: Precondition; } /** * A normalized **delete** operation. * Produced by decoding a GAPIC `IWrite` into the mock’s internal representation. */ export interface NormalizedDelete { /** Discriminator: identifies this operation as a delete. */ type: 'delete'; /** Fully-qualified document path to delete. */ path: string; /** * Optional delete precondition (e.g., must exist, or updateTime must match). * If not met at apply-time, the delete fails with a precondition error. */ precondition?: Precondition; } /** * Controls how a group of normalized writes is executed by the DataAccessor. * * - `Atomic`: Apply all writes as a single commit. Either all succeed or the commit fails. * - `Serial`: Apply writes one-by-one in order, collecting per-write statuses; later writes * still run even if an earlier one fails. */ export declare enum WriteMode { /** Single atomic commit: all-or-nothing semantics. */ Atomic = 0, /** One-by-one application: collect per-write outcomes. */ Serial = 1 } /** * A normalized Firestore write operation. * Internal form used by the mock after decoding GAPIC `IWrite`. * All fields are resolved/preprocessed for application by `DataAccessor`. */ export type NormalizedWrite = NormalizedSet | NormalizedDelete; /** * Outcome for a single write in `WriteMode.Serial`. * Mirrors GAPIC `google.rpc.Status` shape at a high level. */ export interface WriteStatus { /** Numeric status code (e.g., `Status.OK`, `Status.FAILED_PRECONDITION`). */ code?: Status | null; /** Optional diagnostic message associated with the code. */ message?: string | null; } /** * The structured result of applying one or more normalized writes. */ export interface NormalizedWriteResult { /** * The authoritative server time used for this application. * Aligns with commit time for `Atomic`, and with the batch's processing clock for `Serial`. */ serverTime: Timestamp; /** * One `MetaDocument` per affected path in apply order (creates/updates/deletes). * For `Atomic`, represents the final post-commit state. For `Serial`, represents * the state after each write is applied. */ results: MetaDocument[]; /** * Per-write status results, only populated when `mode === WriteMode.Serial`. * The array length equals the number of input writes and indexes align with `results` * entries that reflect the corresponding write’s effect (when any). */ statuses?: WriteStatus[]; } /** * A map of document path → `MetaDocument` describing the changed state for that path. * Typically used to coalesce distinct changes by final path state for listener dispatch. */ export type DataChangeSet = { [path: string]: MetaDocument; }; /** * The argument delivered to change watchers during data change notifications. * Encapsulates the batch’s server time and a lazily-materialized change set. */ export interface DataChangeEventArg { /** * The authoritative server time associated with the emission (commit/application time). */ readonly serverTime: Timestamp; /** * `true` if this is an initial emission (e.g., first subscription snapshot), * `false` for subsequent delta-driven emissions. */ readonly isInitial: boolean; /** * Returns the set of changes keyed by document path, representing the final state per * path for this emission. Implementations may compute this lazily. */ changes(): DataChangeSet; } /** * Callback invoked when a `DataChangeEventArg` is available (e.g., for snapshot listeners). */ export type ChangeWatcher = (arg: DataChangeEventArg) => void; /** * Argument passed to a trigger callback when a matched document changes. * * @public * @remarks * - `params` are the extracted path variables from the matched {@link Trigger.route} * (e.g., for `items/{itemId}`, `params.itemId` is populated). * - `doc` is the meta-document representing the post-commit state for the path, * including existence, update/create/delete semantics, and read/write times as * tracked by the in-memory engine. */ export interface TriggerEventArg { /** * Named parameters captured from the route template. * For example, a route `groups/{gid}/items/{iid}` yields `{ gid, iid }`. */ params: Record; /** * The meta-document affected by the change. * Contains structural and timing metadata used by the mock to emulate Firestore. */ doc: MetaDocument; } /** * Declarative registration for a DataAccessor trigger. * * @public * @remarks * - Triggers are fired by the DataAccessor after an atomic commit resolves. * - The `callback` is invoked synchronously on the mock's microtask queue * (deterministic async), preserving commit order. */ export interface Trigger { /** * Route pattern describing which documents this trigger listens to. * Supports Firestore-like wildcards, e.g. `items/{itemId}` or * `tenants/{tenantId}/orders/{orderId}`. */ route: string; /** * Handler invoked when a document at a matching route changes. * * @param arg - The event payload containing path params and the affected meta-document. */ callback: (arg: TriggerEventArg) => void; } /** * Configuration hooks for the in-memory database. * * @public * @remarks * Provide deterministic time behavior for commits and reads. The mock calls * {@link DatabaseConfig.serverTime} to stamp commit/update/read times so tests * can control clock behavior (e.g., fixed or advancing time). */ export interface DatabaseConfig { /** * Returns the "server" time used to stamp commits and readTimes. * Should be monotonic within a test run. */ serverTime: () => Timestamp; } /** * Internal representation of a structured Firestore query. * * Used by `DataAccessor` to scope and filter documents according to Firestore’s * hierarchy and collection‑group semantics. * * ## Scoping & Resolution Rules * * Let: * - **P** = `parent` (document path or `''` for the database root) * - **A** = `allDescendants` (boolean; defaults to `false` if omitted) * - **C** = `collectionId` (optional) * * The effective search domain is determined by `{P} × {A} × {C}`: * * | Case | `parent` (P) | `allDescendants` (A) | `collectionId` (C) | Search domain | * |------|-----------------------------|-----------------------|--------------------|---------------------------------------------------------------------------------| * | 1 | `''` (DB root) | `true` | **set** | All documents in **every** collection named `C`, anywhere in the database. | * | 2 | `''` (DB root) | `true` | **unset** | All documents in **all** collections (entire database). | * | 3 | `''` (DB root) | `false` | **set** | All documents in **top‑level** collections named `C`. | * | 4 | `''` (DB root) | `false` | **unset** | All documents in **all top‑level** collections (union of immediate children). | * | 5 | document path (e.g. `a/b`) | `true` | **set** | All documents in descendant subcollections named `C` under that document. | * | 6 | document path (e.g. `a/b`) | `true` | **unset** | **All descendant documents** under that document (any collection name). | * | 7 | document path (e.g. `a/b`) | `false` | **set** | All documents in immediate child subcollection `C` of that document. | * | 8 | document path (e.g. `a/b`) | `false` | **unset** | All documents in **any immediate** child subcollection of that document. | * * Notes: * - `parent: ''` corresponds to `.../documents` in GAPIC requests. * - If `collectionId` is **provided**, it filters the domain by name; if **omitted**, * no name filter is applied (a union across collection names). * - Implementations should still enforce any additional query constraints (orderBy, * cursors, limit/offset) *outside* of this scoping step. * * ### Examples * - Sweep all descendants under a doc (used by recursive deletes): * `{ parent: 'col1/doc1', allDescendants: true }` * - Global collection group over `posts`: * `{ parent: '', allDescendants: true, collectionId: 'posts' }` * - Top‑level `users` only: * `{ parent: '', collectionId: 'users' }` * * @template T The shape of stored document data (defaults to `DocumentData`). */ export interface DocumentQuery { /** * The parent path that defines the query scope. * * - **Document path** → scope is beneath that document. * - **Empty string `''` (default)** → scope is the database root (`.../documents`). */ parent?: string; /** * Whether to include all nested collections under `parent`. * * - `true` → include **all descendant** collections (collection‑group semantics). * - `false` or omitted → include **only immediate** child collections of `parent`. */ allDescendants?: boolean; /** * Optional collection ID to restrict by collection name. * * - When provided, only documents from collections with this ID are considered. * - When omitted, documents from **all** collection names in scope are considered. */ collectionId?: string; /** * Point-in-time consistency selector for the query. * * When provided, `DataAccessor` must evaluate the query **as of** this * `readTime` (inclusive): * - Return the latest stored version of each document whose `updateTime` * ≤ `readTime`. * - Exclude documents created after `readTime` and those deleted at or * before `readTime`. * * Notes: * - This value is typically derived from a transaction’s read time or an * explicit `IRunQueryRequest.readTime`. By the time it reaches * `DataAccessor`, it should be the resolved value to use (no additional * inference needed). * - This controls **visibility only**; ordering, cursors, limits, etc. are * applied normally to the visible snapshot. */ readTime?: Timestamp; /** * A predicate applied to each candidate document (post‑scope filtering). * Return `true` to include the document in results. */ predicate: (meta: MetaDocumentExists) => boolean; } /** * A lightweight, immutable snapshot describing the state and lineage of a document * after a single atomic operation (read/write/commit) in the mock datastore. * * Notes: * - Instances are frozen; nested `data` is also frozen. * - Time fields (`serverTime`, `updateTime`, `createTime`) are `Timestamp`s aligned to the * mock's commit/application clock (not the client wall clock). * - `previous` is provided only for write operations that changed the document. */ export interface MetaDocument { /** * Monotonically increasing **per-DataAccessor** epoch identifier. * * - **Increments only when the in-memory database is reset** (never for reads/writes/commits). * - Remains constant for the entire lifetime of a given database instance between resets. * - When combined with {@link version} (the commit sequence number), `(epoch, version)` * provides a stable total ordering key across resets. */ readonly epoch: number; /** * The path of the parent collection for this document (e.g., `"cities/SF/neighborhoods"`). * Always a collection path, never a document path. */ readonly parent: string; /** * The fully-qualified document path (e.g., `"cities/SF"`). */ readonly path: string; /** * The last path segment of `path`; i.e., the document ID (e.g., `"SF"`). */ readonly id: string; /** * `true` if a user document exists at `path` after the associated operation completes; * `false` if the document does not exist (was deleted or never created). */ readonly exists: boolean; /** * The mock server's wall time at which the operation was processed. * This is the authoritative "now" used by server-generated values (e.g., serverTimestamp transforms). */ readonly serverTime: Timestamp; /** * The time that the meta document at `path` was last updated. If no document has ever existed, * then `updateTime.toMillis() === 0`. */ readonly updateTime: Timestamp; /** * The internal, monotonically increasing version (commit sequence number) of the atomic * commit operation (`batchWrite`) that produced this state. * * - If the document was deleted by the operation, `version` is still the commit that processed the delete. * - If the document has never existed, then `version === 0`. */ readonly version: number; /** * The creation time of the document (time of the first successful create/upsert), * or `undefined` if the document has never existed. */ readonly createTime?: Timestamp; /** * A deeply frozen (`Object.freeze`) copy of the document contents after the operation, * or `undefined` if the document does not exist. * * The copy preserves Firestore value types (e.g., `Timestamp`, `GeoPoint`, `Bytes`, `FieldValue` outputs). */ readonly data?: T | undefined; /** * Returns `true` if the initiating data operation resulted in changes to the underlying document * (create, update, transform, or delete). Always returns `false` for pure read operations. */ readonly hasChanges: boolean; /** * The prior state of the document immediately before the change that produced this instance, * provided only when `hasChanges === true`. For read operations or no-ops, this is `undefined`. * * `previous.updateTime` is always less than or equal to `updateTime`. `previous.data` may be * `undefined` if the prior state was "no document". */ readonly previous?: MetaDocument; /** * Returns a **defensive deep clone** of `data` suitable for mutation by callers, * or `undefined` if the document does not exist. The clone is not frozen. * Firestore sentinel/value types (e.g., `Timestamp`, `GeoPoint`) are preserved as instances. */ cloneData(): T | undefined; } /** * A refined `MetaDocument` describing a document that **exists** after the operation. * Narrows `exists` to `true` and guarantees presence of both `createTime` and `data`. * * @typeParam T - The shape of the document data. * * Notes: * - `createTime` is the timestamp of the first successful creation/upsert of the document. * - `data` is a deeply frozen snapshot of the document contents after the operation. */ export interface MetaDocumentExists extends MetaDocument { /** Always `true` for existing documents. */ readonly exists: true; /** The time the document was first created (never `undefined` for existing docs). */ readonly createTime: Timestamp; /** The document's data after the operation (never `undefined` for existing docs). */ readonly data: T; } /** * A refined `MetaDocument` describing a document that **does not exist** after the operation. * Narrows `exists` to `false` and guarantees absence of both `createTime` and `data`. * * @typeParam T - The (potential) shape of the document data when present. * * Notes: * - `createTime` is absent when the document has never existed or has been deleted. * - `data` is absent because there is no stored document at `path`. */ export interface MetaDocumentNotExists extends MetaDocument { /** Always `false` for non-existent documents. */ readonly exists: false; /** Absent because the document does not exist. */ readonly createTime?: undefined; /** Absent because the document does not exist. */ readonly data?: undefined; } /** * Async façade over `DataAccessor` that enforces deterministic asynchronous I/O. * * All methods delegate to the underlying synchronous/async `DataAccessor` and * wrap the result via `resolvePromise(...)` to guarantee: * - microtask-based asynchrony (no sync callbacks), * - consistent error propagation through Promises, * - parity with Firestore’s event-loop-driven behavior. */ declare class _AsyncDataAccessor { private readonly _inner; /** * @param _inner The underlying, high-fidelity in-memory `DataAccessor`. */ constructor(_inner: DataAccessor); /** * Lists the IDs of **direct child collections** under a document path. * * @param documentPath - Fully-qualified document path (e.g., `"cities/SF"`). * @returns A Promise resolving to the distinct collection IDs (no duplicates). */ listCollectionIds(documentPath: string): Promise; /** * Lists documents in a collection. * * @param collectionPath - Fully-qualified collection path (e.g., `"cities"` or `"cities/SF/areas"`). * @param showMissing - If `true`, include non-existent “placeholder” meta-documents * (useful for structural introspection). If `false`, only existing documents are returned. * @returns A Promise resolving to meta-documents in the collection. */ listDocuments(collectionPath: string, showMissing: boolean): Promise; /** * Executes a structured document query and returns **existing** documents only. * * @typeParam T - The shape of the document data. * @param q - The normalized, validated `DocumentQuery`. * @returns A Promise resolving to an array of `MetaDocumentExists`, ordered per query. */ query(q: DocumentQuery): Promise[]>; /** * Reads a single document, optionally **as of** a past `readTime`. * * If `readTime` is provided, returns the state closest to (≤) that timestamp, * or a non-existent meta-document if the document did not exist at/ before that time. * * @typeParam T - The shape of the document data. * @param documentPath - Fully-qualified document path. * @param readTime - Optional historical read time. * @returns A Promise resolving to a `MetaDocument` describing the doc state. */ getDoc(documentPath: string, readTime?: Timestamp): Promise>; /** * Applies a batch of normalized writes. * * - `WriteMode.Atomic`: all-or-nothing commit semantics. * - `WriteMode.Serial`: apply sequentially and report per-write statuses. * * Server time used for transforms and timestamps is exposed on the result. * * @param ops - Normalized write operations to apply. * @param mode - Write execution mode (`Atomic` or `Serial`). * @returns A Promise resolving to the structured write result. */ batchWrite(ops: NormalizedWrite[], mode: WriteMode): Promise; } /** * Public alias for the async `DataAccessor` façade. * * Prefer this type in consumer-facing signatures to avoid exporting the * private class name. */ export type AsyncDataAccessor = _AsyncDataAccessor; export declare class DataAccessor implements PathDataProvider { /** * Monotonically increasing **per-DataAccessor** epoch identifier. * Increments only when the in-memory database is reset (never for reads/writes/commits). * Remains constant for the entire lifetime of a given database instance between resets. */ private _epoch; /** * The monotonically increasing atomic commit version (increments with each atomic `batchWrite`). * Each document change (create/update/delete) carries the version associated with its corresponding * `batchWrite`. */ private _version; private readonly _resetListeners; private readonly _statsWatchers; private readonly _changeWatchers; private readonly _triggers; private _pathCache; private _stats; private _txs; private _src; /** * Returns the current "server" time. */ readonly serverTime: () => Timestamp; /** * Async façade over `DataAccessor` that enforces deterministic asynchronous I/O. */ readonly async: AsyncDataAccessor; constructor(config: DatabaseConfig); /** * The monotonically increasing **per-DataAccessor** epoch identifier. * * - **Increments only when the in-memory database is reset** (never for reads/writes/commits). * - Remains constant for the entire lifetime of a given database instance between resets. * - When combined with {@link version} (the commit sequence number), `(epoch, version)` * provides a stable total ordering key across resets. */ get epoch(): number; /** * The monotonically increasing atomic commit version (increments with each atomic `batchWrite`). * Each document change (create/update/delete) carries the version associated with its corresponding * `batchWrite`. */ get version(): number; /** * Returns cached structural metadata for a given path, optionally asserting its type. * * @param path - A canonical path string (e.g., `'items/i1'`, `'users/u1/posts'`). * @param guards - Optional type guards that must match the path (e.g., `'document'`, `'collection'`, `'root'`). * @returns The {@link PathData} for the path if it exists in the cache and satisfies the guards; otherwise `undefined`. * * @remarks * - This is a pure cache lookup via the internal `PathDataCache`. * - When `guards` are provided, the path must satisfy **all** requested types or `undefined` is returned. * - No I/O occurs and the database state is not mutated. */ pathData(path: string, ...guards: PathType[]): PathData | undefined; /** * Registers a listener that fires whenever the in-memory database is reset. * * @param callback - Invoked after a reset operation completes. * @returns An unsubscribe function that removes the listener. * * @remarks * - Reset events typically occur in tests to clear state between cases. * - The callback is invoked on the mock’s deterministic microtask queue. * - Multiple listeners are supported; order of invocation is unspecified. */ registerResetListener(callback: (epoch: number) => void): () => void; /** * Registers a low-level change watcher and immediately delivers a synthetic * "current state" snapshot followed by future change notifications. * * @param watcher - The {@link ChangeWatcher} to receive change batches. * @returns An unsubscribe function that de-registers the watcher. * * @remarks * - Upon registration, the watcher is primed by calling * {@link processChangeWatchers} with all existing documents (a sync pass), * using the current {@link serverTime}. * - Subsequent atomic commits produce batched change notifications in commit order. * - The unsubscribe function is idempotent. */ registerChangeWatcher(watcher: ChangeWatcher): () => void; /** * Registers a route-based trigger that fires after atomic commits affecting matching documents. * * @param trigger - The trigger definition (route + callback). A shallow copy is stored internally. * @returns An unsubscribe function that removes the trigger. * * @remarks * - Routes use Firestore-like wildcards (e.g., `items/{itemId}`). * - Triggers observe **post-commit** state and fire on the mock’s microtask queue. * - If multiple writes target the same document in a single commit, only the * **final** state for that path is delivered to the trigger. */ registerTrigger(trigger: Trigger): () => void; /** * Lists the IDs of direct child collections beneath a document (or the root). * * @param documentPath - A document path (e.g., `users/u1`) or root sentinel if supported. * @returns An array of child collection IDs, filtered to those with active leaf documents. * * @throws {Error} If `documentPath` is not a document (or allowed root), as asserted by the path cache. * * @remarks * - Only collections that currently contain at least one active document are returned. * - This mirrors `DocumentReference.listCollections()` behavior at a structural level. */ listCollectionIds(documentPath: string): string[]; /** * Produces a sorted array of meta-documents representing all existing documents. * * @returns An array of {@link MetaDocumentExists}, sorted lexicographically by `path`. * * @remarks * - Each element is computed via {@link readMetaDoc} using the current {@link serverTime}. * - Only documents with `exists === true` are included. * - Useful for assertions and structural introspection in tests. */ toMetaArray(): MetaDocumentExists[]; /** * Returns a map of document path → meta-document for all existing documents. * * @returns A plain object whose keys are canonical document paths and values are {@link MetaDocumentExists}. * * @remarks * - Meta entries are materialized using {@link readMetaDoc} with the current {@link serverTime}. * - Only documents with `exists === true` are included. * - Order is not guaranteed; use {@link toMetaArray} for a stable, sorted view. */ toMetaMap(): Record; /** * Returns a map of document path → cloned document data for all existing documents. * * @returns A plain object whose keys are canonical document paths and values are deep-cloned {@link DocumentData}. * * @remarks * - Only documents with `exists === true` are included. * - Values are cloned via {@link cloneDocumentData} to prevent accidental mutation of internal state. * - Intended for test visibility and debugging; not optimized for very large datasets. */ toMap(): Record; /** * Materializes a **read-only structural snapshot** of the entire in-memory database * as a tree of collections → documents → (nested collections), rooted at `''`. * * @returns A {@link StructuralDatabase} object representing the current state. * * @remarks * - Only documents with `exists === true` are included. * - Each document’s `data` is deep-cloned (no shared references with internal state). * - The returned structure is suitable for serialization, diffing, or round-tripping * back into the database via {@link fromStructuralDatabase}. * * @example * ```ts * // Shape example (collections → documents → data|collections): * const tree = acc.toStructuralDatabase(); * // { * // users: { * // u1: { * // data: { name: 'Ada' }, * // collections: { * // posts: { * // p1: { data: { title: 'Hello' } } * // } * // } * // } * // } * // } * ``` */ toStructuralDatabase(): StructuralDatabase; /** * Ingests a {@link StructuralDatabase} tree and applies it to the in-memory * database via a single atomic batch write. * * @param data - The structural tree to import (collections → documents → data / nested collections). * @param merge - Optional merge granularity for `set` operations; defaults to `'root'`. * @returns A {@link NormalizedWriteResult} describing the applied atomic commit. * * @remarks * - Each structural document with a `data` property becomes a `set` write at its path. * - Nested `collections` are traversed depth-first; the resulting writes are committed atomically. * - `merge` controls how document data is applied (see {@link MergeGranularity} semantics). * - This is ideal for seeding fixtures, restoring snapshots, or constructing complex hierarchies * in tests with deterministic commit semantics. * * @example * ```ts * const fixtures: StructuralDatabase = { * users: { * u1: { data: { name: 'Ada' } }, * u2: { * data: { name: 'Grace' }, * collections: { * posts: { * p1: { data: { title: 'Hello' } } * } * } * } * } * }; * * // Apply as a single atomic commit * const result = acc.fromStructuralDatabase(fixtures, 'root'); * ``` */ fromStructuralDatabase(data: StructuralDatabase, merge?: MergeGranularity): NormalizedWriteResult; /** * Lists meta-documents under a given collection path. * * @param collectionPath - Canonical collection path (e.g., `"users"`, `"users/u1/posts"`). * @param showMissing - When `true`, include *missing* documents that have active child collections * (i.e., documents that don't exist but have nested subcollections). When `false`, only existing * documents are returned. * @returns An array of {@link MetaDocument} describing each document’s post-commit state at the * current {@link serverTime}. For missing documents included due to `showMissing`, `exists === false`. * * @throws {Error} If `collectionPath` is not a collection path (validated by the path cache). * * @remarks * - Uses the in-memory structural index; no network I/O. * - When `showMissing` is `true`, a document is included if it has **active leaf docs** in a * descendant collection, even if the document itself is not stored. * - Read accounting: only **existing** documents contribute to read counts; missing documents are * not charged. This is applied via an aggregated `bumpReads()` call. * - Each returned meta-document is produced by `readMetaDoc(serverTime, …, showMissing)` to embed * consistent timing and existence metadata. * * @example * ```ts * // Only existing docs in a collection * const existingOnly = acc.listDocuments('users', false); * // Existing + synthetic "missing" docs that have subcollections * const withMissing = acc.listDocuments('users', true); * * // Filter existing docs * const existing = withMissing.filter(d => d.exists); * ``` */ listDocuments(collectionPath: string, showMissing: boolean): MetaDocument[]; /** * CONTRACT: DataAccessor.query * -------------------------------- * Role * - Enumerate candidate documents from the in-memory tree according to: * (a) scope (`parent`), (b) depth (`allDescendants`), (c) name filter (`collectionId`), * (d) point-in-time visibility (`readTime`), and (e) a WHERE-like `predicate(data)`. * * Inputs * - `parent`: '' for database root, or a document path. Already validated upstream. * - `allDescendants`: if true, walk all descendant collections; else only immediate child collections. * - `collectionId` (optional): when set, only *collect* docs from collections with this ID at the current depth. * IMPORTANT: This does NOT restrict recursion; traversal must continue so deeper matches aren’t missed. * - `readTime` (optional): evaluate existence and data **as of** this timestamp. * Uses ghost nodes + `MasterDocument.getSnapshot(now, readTime)` for point-in-time views. * - `predicate(data)`: pure function implementing WHERE logic; called only for documents visible at `readTime`. * * Guarantees * - Returns a flat array of `MetaDocumentExists` where each item: * • is within scope rooted at `parent` * • is visible at `readTime` (or “now” if absent) * • originates from a collection whose name matches `collectionId` at the *collection where it was found*, * when `collectionId` is provided * • satisfies `predicate(data)` * - Traverses subcollections even when a parent document doesn’t exist (now or at `readTime`). * - No reliance on “active” flags when `readTime` is present (they’re “now” signals only). * - Does not deduplicate (tree contains unique document nodes); every returned meta has `exists === true`. * - **Ordering is undefined** (implementation detail). Callers must not rely on output order. * * Non-Responsibilities (handled by higher layers / execution plan) * - `orderBy` (including implicit `__name__`), `startAt` / `endAt` cursors, * `offset` / `limit`, field projection (`select`), streaming/envelope, and aggregation computation. * * Error / Edge Semantics * - If `parent` does not resolve to a node, returns `[]`. * - Assumes request validation is performed upstream (paths, mutually exclusive consistency selectors, etc.). * * Performance Notes * - It’s safe to short-circuit via “active” flags **only when** `readTime` is undefined. * - Prefer single enumeration of each collection’s iterator (buffer once; collect then recurse). */ query(q: DocumentQuery): MetaDocumentExists[]; /** * Reads a meta-document at the given path, optionally as-of a specific {@link Timestamp}. * * @typeParam T - The document’s data shape (defaults to {@link DocumentData}). * @param documentPath - Canonical document path, e.g. `"users/u1"` or `"users/u1/posts/p1"`. * @param readTime - Optional historical read time. When provided, returns the best-known * snapshot materialized at (or before) this time according to the mock’s version store. * When omitted, returns the latest state. * @returns A {@link MetaDocument} describing existence, times, and data (typed as `T`). * * @remarks * - Delegates to `MasterDocument.get(...)` with the current `serverTime()` and optional `readTime`. * - Read accounting: only **existing** documents increment read counts; missing docs do not. * - No mutation occurs. The returned `meta.data` is safe to consume but should be treated as read-only. * * @example * ```ts * // Latest state * const meta = acc.getDoc<{name: string}>('users/u1'); * if (meta.exists) { * console.log(meta.data!.name); * } * * // Historical read * const asOf = Timestamp.fromMillis(t0); * const old = acc.getDoc('users/u1', asOf); * ``` */ getDoc(documentPath: string, readTime?: Timestamp): MetaDocument; /** * Returns whether a document currently exists at the given path. * * @param documentPath - Canonical document path, e.g. `"users/u1"`. * @returns `true` if the document exists **now**; otherwise `false`. * * @remarks * - This is a lightweight structural check against the in-memory index. * - No read accounting is performed and no historical read is supported here. * Use {@link getDoc} with `readTime` for time-travel queries. * * @example * ```ts * if (!acc.docExists('users/u1')) { * await db.doc('users/u1').set({ created: true }); * } * ``` */ docExists(documentPath: string): boolean; /** * Returns `true` if `documentInfo` accurately reflects the referenced document's current state * in the database, otherwise returns `false. */ docIsCurrent(documentInfo: MetaDocument): boolean; /** * Returns `true` if every `DocumentInfo` item in `info` accurately reflects the referenced document's * current state in the database, otherwise returns `false. */ docsAreCurrent(info: MetaDocument[]): boolean; /** * Applies a batch of normalized writes to the in-memory store, either **atomically** * or in **serial** mode with per-op status reporting. * * @param ops - An ordered array of normalized writes (each with `path`, `type`, optional * `data`, and optional `precondition`). * @param mode - Write behavior: `WriteMode.Atomic` or `WriteMode.Serial`. * @returns A {@link NormalizedWriteResult} containing: * - `serverTime`: the commit timestamp used for all writes in this batch, * - `results`: an array of {@link MetaDocument} in the **same order** as `ops`, * - `statuses` (Serial mode only): per-op {@link WriteStatus} with `code`/`message` or `OK`. * * @remarks * ### Modes * - **Atomic**: The batch is validated and applied as a single unit. On the first * failing precondition or validation, a {@link GoogleError} is thrown and **no** * changes are committed. * - **Serial**: Each operation is independently validated/applied in order. For a * failing op, an entry is pushed into `statuses` with the error code/message and the * op is skipped; subsequent ops continue. Successful ops receive `Status.OK`. * * ### Preconditions * For each operation, these checks are performed against the buffered view: * - `exists === false` → error `ALREADY_EXISTS` if the document currently exists. * - `exists === true` → error `NOT_FOUND` if the document is missing. * - `lastUpdateTime` → error `FAILED_PRECONDITION` if it does not match current `updateTime`. * * ### Validation & Transforms (`set` only) * - `data` must be defined (`INVALID_ARGUMENT` if not). * - Field transforms are applied (`applyTransformers`) using the batch `serverTime`. * - Data is normalized to Firestore's wire-compatible representation. * - Structural/size guards run (`assertMaxDepth`, `assertWithinSizeLimit`). * * ### Execution Model * - A per-document buffer is built first so multiple ops on the same path coalesce, * with the **final** state (exists/data/updateTime) used when committing. * - After successful buffering, changes are applied to the store in op order: * `set` → `MasterDocument.set(...)`; otherwise `MasterDocument.delete(...)`. * - Each applied op yields a {@link MetaDocument} pushed to `results` at the same index * as the originating op. * - `enqueueChanges(serverTime)` is invoked once at the end to notify watchers/triggers. * - The internal store version increments once per `batchWrite` call. * * ### Error Semantics * - **Atomic**: throws a {@link GoogleError} immediately on first failure; no partial writes. * - **Serial**: records each failure into `statuses` and skips that op; does **not** throw * for per-op failures. Non-`GoogleError` exceptions are wrapped as `INTERNAL`. * * @example * ```ts * // Atomic: all-or-nothing * const atomic = acc.batchWrite( * [ * { path: 'users/u1', type: 'set', data: { name: 'Ada' } }, * { path: 'users/u2', type: 'delete' }, * ], * WriteMode.Atomic * ); * // atomic.results[0].exists === true * * // Serial: continue after failures, inspect per-op statuses * const serial = acc.batchWrite( * [ * { path: 'users/u3', type: 'set', data: { name: 'Grace' } }, * { path: 'users/u3', type: 'set', } as any, // missing data → INVALID_ARGUMENT * { path: 'users/uX', type: 'delete', precondition: { exists: true } }, // may fail * ], * WriteMode.Serial * ); * // serial.statuses → [{ code: OK }, { code: INVALID_ARGUMENT, ... }, { code: NOT_FOUND, ... }] * ``` */ batchWrite(ops: NormalizedWrite[], mode: WriteMode): NormalizedWriteResult; /** * Returns consolidated database statistics. * * @returns A {@link DatabaseStats} object combining: * - **Operational counters** from `_src.stats` (e.g., reads/writes as tracked by the engine), and * - **Structural counters** computed on demand (documents/collections and their structural stubs). * * @remarks * - Structural stats are computed lazily and memoized in `_stats` until the next invalidation * (e.g., after writes/resets elsewhere in the DataAccessor). The method merges a **fresh** * structural snapshot with the current operational stats at call time. * - **Stub** items: * - `stubDocumentCount`: non-existent documents that anchor subcollections with active leaf docs. * - `stubCollectionCount`: collections with no immediate docs but with active descendants. * - The synthetic root is excluded from document counts. * * @example * ```ts * const stats = acc.getStats(); * // stats.documentCount, stats.collectionCount, stats.stubDocumentCount, stats.stubCollectionCount * // stats.readCount, stats.writeCount, ... (from _src.stats) * ``` */ getStats(): DatabaseStats; /** * Subscribes to database stat updates. * * @param watcher - Callback invoked with the latest {@link DatabaseStats}. * @returns An unsubscribe function to stop receiving updates. * * @remarks * - The `watcher` is called **immediately** with `getStats()` to prime consumers. * - Subsequent updates are delivered according to the DataAccessor’s internal * stats invalidation/notification flow (e.g., after writes or resets). * * @example * ```ts * const unsub = acc.watchStats(s => console.log('Stats:', s)); * // ... * unsub(); * ``` */ watchStats(watcher: (stats: DatabaseStats) => void): () => void; /** * Resolves a transaction handle from a flexible resolver shape. * * @param resolver - A {@link ResolveTransactionShape} describing how to locate the transaction, * or `undefined` to indicate no active transaction. * @returns The matching {@link InternalTransaction}, or `undefined` if `resolver` is undefined * (or does not match any active transaction depending on resolver semantics). * * @remarks * - Intended for internal plumbing where APIs may accept either a transaction object, * identifier, or a sentinel indicating “no transaction”. */ resolveTransaction(resolver: ResolveTransactionShape | undefined): InternalTransaction | undefined; /** * Begins a new transaction. * * @param options - Optional {@link ITransactionOptions} controlling transaction behavior. * @returns An {@link InternalTransaction} representing the opened transaction. * * @remarks * - The returned handle should be used with subsequent read/write operations and then * committed/rolled back by the coordinating layer. */ begin(options?: ITransactionOptions | null | undefined): InternalTransaction; /** * Fetches an existing transaction by its identifier. * * @param transactionId - The binary identifier of the transaction. * @returns The {@link InternalTransaction} associated with the identifier. * * @remarks * - Use when resuming/continuing work in a known transaction context. * - Behavior for unknown identifiers is implementation-defined (may throw). */ fetch(transactionId: Buffer): InternalTransaction; /** * Clears the database without performing a `reset`. This method: * - Deletes all documents and collections * - Flushes all pending changes without invoking watcher callbacks * - Sets the the database stats `collectionCount` and `documentCount` properties to `0` * */ clear(): void; /** * Resets the database. This method: * - Deletes all documents and collections * - Flushes all pending changes without invoking watcher callbacks * - Sets all database stats properties to `0` * - Resets the internal change version nonce (assigned to `MetaDocument` instances) to `0` */ reset(): void; /** * Enqueues processing of pending document changes on the next microtask tick. * * This method is called after each atomic mutation (e.g. via `commit`, `batchWrite`) * to deliver change notifications to snapshot listeners. * * Changes are drained from the internal `_src.changes` queue and passed to * `processChanges()` within a microtask, ensuring asynchronous and deterministic * listener notification timing consistent with Firestore semantics. */ private enqueueChanges; /** * Processes document changes and notifies matching snapshot watchers. * * This method is invoked after atomic write operations that may affect registered * `DocumentWatcher`s. Each watcher defines a `path` * and `predicate` function to evaluate whether it should be notified of the change. * * @param changes - The list of MetaDocument instances representing applied document changes. * Each MetaDocument may represent a create, update, or delete event. */ private processChanges; /** * Notifies change watchers with a normalized change-set. * * @param serverTime - The commit-time used for the associated changes. * @param changes - The per-document meta changes to surface. * @param watcher - When provided, only this watcher is invoked and `isInitial` is `true`. * When omitted, all registered watchers are notified and `isInitial` is `false`. * * @remarks * - The event payload exposes `changes()` which **lazily** materializes a frozen * `DataChangeSet` (path → `MetaDocument`) on first access, avoiding work if * a subscriber never inspects it. * - When `watcher` is specified, this method acts as a *priming* call for that * single watcher (used on registration). Otherwise, it broadcasts to all. * - The `DataChangeEventArg` object itself is frozen to prevent subscriber mutation. */ private processChangeWatchers; /** * Dispatches route-based triggers for a batch of document changes. * * @param triggers - The candidate triggers to evaluate. * @param changes - The raw, ordered list of meta-doc changes produced by the commit. * * @remarks * - If multiple operations target the **same document path** within the batch, * only the **last** one is delivered to triggers (coalescing), while preserving * the original ordering of those last occurrences across distinct paths. * - Trigger matching uses `matchFirestorePath(route, path)` (Firestore-like wildcards). * - Each trigger callback is scheduled via `queueMicrotask` to (a) unwind the * current stack, and (b) isolate subscriber exceptions from the writer. * - The `params` map is populated from `{param}` segments of the matched route. */ private processTriggers; /** * Returns a snapshot of all **existing** documents as meta-docs. * * @returns An array of {@link MetaDocument} for every document with `exists === true`. * * @remarks * - Uses the current {@link serverTime} to materialize meta fields consistently. * - Missing documents and structural stubs are **not** included. * - Intended to prime watchers or support structural introspection. */ private all; /** * Constructs an immutable operation context for a single datastore action. * * The context captures the current **epoch**, **commit sequence `version`**, * a point-in-time **`serverTime`**, and the active **datasource** backing * this `DataAccessor`. Use this to stamp reads/writes with consistent, * authoritative metadata. * * @param incVersion - When `true`, atomically **increments** the internal * commit sequence counter and returns the **new** value in `version`. * When `false` (default), returns the **current** `version` without changing it. * - Pass `true` for operations that **apply a write/commit**. * - Pass `false` for **pure reads** or preflight checks. * * @returns An {@link OpContext} object: * - `epoch`: Monotonically increasing identifier for this accessor’s lifecycle. * - `version`: Commit sequence number (incremented only when `incVersion` is `true`). * - `serverTime`: The mock server’s authoritative timestamp for the operation. * - `datasource`: The underlying in-memory source used to resolve the operation. * * @remarks * - `(epoch, version)` forms a total ordering key across database resets. * - `serverTime` is captured at call time and should be used for transform * evaluation and timestamp fields to ensure deterministic results. */ private opContext; } export declare function deepDocumentDataEqual(a: unknown, b: unknown): boolean; /** * Normalizes a POJO to match Firestore persistence behavior: * - Removes undefined values * - Preserves arrays (even if empty) */ export declare function normalizeFirestoreData(input: any): any; export interface IReadOnly { /** ReadOnly readTime */ readTime?: Timestamp | null | undefined; } export interface IReadWrite { /** ReadWrite retryTransaction */ retryTransaction?: Buffer | null; } export interface ITransactionOptions { /** TransactionOptions readOnly */ readOnly?: IReadOnly | null | undefined; /** TransactionOptions readWrite */ readWrite?: IReadWrite | null | undefined; } export interface ITransactionWrites { writes: NormalizedWrite[]; mode: WriteMode; } /** * Represents a base64-encoded transaction ID. */ export type TransactionId = string; export type TransactionType = 'readOnly' | 'readWrite'; export declare enum TransactionStatus { Active = 0, Committed = 1, Aborted = 2 } export declare class InternalTransaction { readonly type: TransactionType; readonly retryKey: string | undefined; private _status; private _readTime; private _created; private _touched; private readonly _accessor; private readonly _map; private readonly _readSet; readonly id: Buffer; readonly sId: string; constructor(accessor: DataAccessor, map: Map, type: TransactionType, readTime: Timestamp | undefined, retryKey: string | undefined); get status(): TransactionStatus; get readTime(): Timestamp | undefined; static ensureReadTime(accessor: DataAccessor, transaction: InternalTransaction | undefined): Timestamp; commit(writes?: ITransactionWrites): NormalizedWriteResult; rollback(): void; registerRead(doc: MetaDocument): void; isTimedOut(now: number): boolean; touch(): void; private complete; private setStatus; } export interface ResolveTransactionShape { newTransaction?: ITransactionOptions | null | undefined; transaction?: Buffer | null | undefined; } export declare function validateTransactionOptions(options: ITransactionOptions | null | undefined): void; export {}; //# sourceMappingURL=data-accessor.d.ts.map