/** * Tina4 DocStore - pymongo-style document storage with a zero-config SQLite (JSON1) fallback. * * A document store with the everyday MongoDB driver collection API, backed by * SQLite's JSON1 extension when no MongoDB server is configured. * * import { getCollection, ObjectId } from "@tina4/orm"; * * const orders = await getCollection("orders"); // SqliteCollection when no Mongo configured * const { insertedId } = await orders.insertOne({ customer_id: 1, total: 9.99 }); * for (const o of await orders.find({ customer_id: { $in: [1, 2] } }).sort("created_at", -1).limit(10).toArray()) { * // ... * } * await orders.updateOne({ _id: insertedId }, { $set: { status: "shipped" } }); * * `getCollection(name)` returns a real MongoDB driver `Collection` when a Mongo * URI is configured (TINA4_MONGO_URI, else TINA4_SESSION_MONGO_URI - the same * names the queue/session Mongo backends read), and otherwise a SqliteCollection * backed by a local SQLite file. This mirrors the file-based fallbacks the queue, * cache, and session subsystems already provide: an app that talks to Mongo in * production runs serverless in local dev with no code change - only the backend * differs. * * A configured URI with NO driver installed throws `DocStoreDriverMissing` * (ADR-0033). It does NOT quietly use the local SQLite store, and it no longer * surfaces a bare ERR_MODULE_NOT_FOUND that names an npm package rather than * the framework decision that led there. * * Design (the SQLite backend): * - Each collection is a table `(_id TEXT PRIMARY KEY, doc TEXT)`; `doc` is JSON. * - Query filters are pushed down to SQL over `json_extract(doc, '$.field')` * (lazy, not a full in-memory scan), supporting equality, $in/$nin, * $gt/$gte/$lt/$lte, $ne, $exists, $regex, and implicit-AND / $or / $and. * - Updates: $set, $unset, $inc, and full-document replace. * - Cursors: sort / limit / skip / projection. * - IDs are a built-in 12-byte ObjectId (zero-dependency; interchangeable with * the driver's ObjectId as a 24-hex string). * * Type round-trip is by value, not by wrapper object, so json_extract stays * queryable and sortable: a Date is stored as an ISO-8601 UTC string and an * ObjectId as its 24-hex string, and reads rehydrate a strict-ISO string back to * a Date and a 24-hex string back to an ObjectId. That keeps range queries and * sorts working on date and id fields - the trade-off (a plain 24-hex / ISO * string becomes an ObjectId / Date on read) is acceptable for the local dev store. * * Deliberate non-goals: aggregation pipeline, $elemMatch, geo. This is the * everyday CRUD + filter subset, not full Mongo parity. */ import { DatabaseSync } from "node:sqlite"; /** Raised when a value cannot be parsed as an ObjectId. */ export declare class InvalidId extends Error { constructor(message: string); } /** * A 12-byte MongoDB-style ObjectId, with no external dependency. * * Layout: 4-byte big-endian seconds since epoch, 5-byte per-process random, * 3-byte big-endian counter. Renders as a 24-char hex string, so it is * interchangeable with the driver's ObjectId wherever the string form is used. */ export declare class ObjectId { private static _counter; private static _process; private readonly _bytes; constructor(oid?: ObjectId | Buffer | Uint8Array | string | null); private static _generate; static isValid(value: unknown): boolean; get binary(): Buffer; /** The timestamp embedded in the id (the first 4 bytes), as a Date. */ get generationTime(): Date; toString(): string; toJSON(): string; equals(other: unknown): boolean; } /** Value -> JSON-serialisable, sortable scalar form (for storage/queries). */ export declare function encodeValue(value: unknown): unknown; /** Stored JSON value -> rich value, rehydrating ObjectId (24-hex) and Date (ISO). */ export declare function decodeValue(value: unknown): unknown; interface CompiledFilter { where: string; params: unknown[]; } /** * Compile a Mongo-style filter object into { where, params }. * * Returns { where: "1=1", params: [] } for an empty filter. Supports implicit * AND across keys, $or / $and, and the per-field operator set. */ export declare function compileFilter(query?: Record | null): CompiledFilter; export interface InsertOneResult { acknowledged: boolean; insertedId: unknown; } export interface InsertManyResult { acknowledged: boolean; insertedIds: unknown[]; } export interface UpdateResult { acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: unknown | null; } export interface DeleteResult { acknowledged: boolean; deletedCount: number; } /** Lazy result cursor. Builds and runs SQL only when materialised (toArray). */ /** The three sort spellings a real FindCursor accepts. */ export type SortSpec = string | [string, number][] | Record | Map; /** * Normalise the driver's three sort spellings to a list of [key, direction]. * * ADR-0036. A real `FindCursor.sort()` accepts a key plus a direction, a list * of `[key, direction]` pairs, OR an object/Map - and the driver is the shape * this fallback imitates (ADR-0025). The object form used to throw * `TypeError: keyOrList is not iterable` here. Measured 2026-08-04 against a * real MongoDB: the object spelling worked on the driver and threw on the * fallback, in three of the four frameworks. */ export declare function sortSpec(keyOrList: SortSpec, direction?: number): [string, number][]; export declare class Cursor { #private; /** * The cursor receives WHAT IT NEEDS, not the collection it came from. * * It used to hold the collection and reach back for `connection`, `quoted` and * `load` - which is the only reason those three were public. ADR-0025 * corollary 1: anything the fallback needs internally is private, and a real * FindCursor exposes none of them. Handing over the two values and calling the * module-level loader removes the back-reference AND the public surface. */ constructor(conn: DatabaseSync, quoted: string, where: string, params: unknown[], projection?: Record | null); sort(keyOrList: SortSpec, direction?: number): this; limit(n: number): this; skip(n: number): this; /** * Materialise the cursor into an array of decoded documents. * * ASYNC because the driver's FindCursor.toArray() is async (ADR-0025 clause * 3). The work underneath is synchronous - node:sqlite has no async API - but * the SHAPE is what a call site sees, and a shape that changes with the * provider is the defect this fixes. */ toArray(): Promise[]>; /** * Async iteration, matching the driver. * * NOTE: there is deliberately no [Symbol.iterator] here. A real FindCursor * has ONLY Symbol.asyncIterator, so `for (const doc of cursor)` is a * fallback-only spelling - it works locally and throws "is not iterable" the * moment TINA4_MONGO_URI is set. Use `for await (const doc of cursor)`. * * toList() is gone for the same reason: the driver's FindCursor has no such * method. * * ADR-0035 restored the uniform spellings in ruby and php through a * delegator, and deliberately did NOT do so here. A delegator can only supply * a method that is POSSIBLE on the real provider, and a synchronous iterator * is not: a FindCursor is async-only. Adding one back on the fallback alone * would recreate ADR-0025's worst measured defect - identical source changing * TYPE, with a truthy Promise passing `if (doc)` for a document that does not * exist. That is ADR-0025 corollary 3, which ADR-0035 keeps. */ [Symbol.asyncIterator](): AsyncIterator>; } /** A SQLite-backed collection exposing the everyday MongoDB driver API. */ export declare class SqliteCollection { #private; constructor(conn: DatabaseSync, name: string); insertOne(document: Record): Promise; insertMany(documents: Record[]): Promise; find(filter?: Record | null, projection?: Record | null): Cursor; findOne(filter?: Record | null, projection?: Record | null): Promise | null>; countDocuments(filter?: Record | null): Promise; estimatedDocumentCount(): Promise; distinct(key: string, filter?: Record | null): Promise; updateOne(filter: Record | null | undefined, update: Record, options?: { upsert?: boolean; }): Promise; updateMany(filter: Record | null | undefined, update: Record, options?: { upsert?: boolean; }): Promise; replaceOne(filter: Record | null | undefined, replacement: Record, options?: { upsert?: boolean; }): Promise; deleteOne(filter?: Record | null): Promise; deleteMany(filter?: Record | null): Promise; drop(): Promise; } /** A SQLite-backed document database (a file of collection tables). */ export declare class SqliteDatabase { readonly path: string; private readonly conn; private readonly collections; constructor(path?: string); getCollection(name: string): SqliteCollection; listCollectionNames(): string[]; close(): void; } /** * A Mongo URI is configured but the MongoDB driver is not installed. * * ADR-0024 rule 3, settled for DocStore by ADR-0033: a provider that cannot * honour an operation must RAISE, naming the provider and what is missing. * Node already threw here, but with a bare ERR_MODULE_NOT_FOUND that named an * npm package and not the framework decision that led there - so the outcome * was loud but undocumented, and different from the other three frameworks. */ export declare class DocStoreDriverMissing extends Error { constructor(message: string); } /** True when no Mongo is configured, so the SQLite fallback is in effect. */ export declare function isServerless(): boolean; /** * Return a collection for `name`. * * A real MongoDB driver `Collection` when a Mongo URI is configured (and the * `mongodb` driver is installed); otherwise a `SqliteCollection` backed by the * local SQLite file. Same call sites either way - only the backend differs. * * ALWAYS async, on BOTH providers (ADR-0025 clause 3). * * It used to return a SqliteCollection SYNCHRONOUSLY in serverless mode and a * Promise on the real-Mongo path. That made identical source change TYPE when * TINA4_MONGO_URI was set, and a Promise is always truthy - so un-awaited code * read a real document locally and a thenable in production, and `if (doc)` * succeeded for a document that did not exist. The driver cannot become sync, * so the fallback becomes async. */ export declare function getCollection(name: string): Promise; /** * Close every DocStore connection: the SQLite store and all Mongo clients. * * A pooled client keeps the event loop alive, so a script or test that touches * the real provider needs a way to let the process end on its own. */ export declare function closeDocStore(): Promise; /** Drop the cached default SQLite store (test helper). */ export declare function resetDefaultStore(): void; export {};