@web-ts-toolkit/mongoose-rxdb
A Mongoose-shaped API (Schema, Document, Query, Model, Connection, pre/post middleware)
backed by RxDB so your data lives in local SQLite (or any RxDB storage).
It is a read-like-Mongoose, persists-offline proxy: schema definitions, casting, validation, dirty
tracking, virtuals, methods, statics, chainable thenable queries, and pre/post hooks all run
against an RxDB collection.
Installation
- npm
- Yarn
- pnpm
- Bun
npm install @web-ts-toolkit/mongoose-rxdb rxdb rxjs
yarn add @web-ts-toolkit/mongoose-rxdb rxdb rxjs
pnpm add @web-ts-toolkit/mongoose-rxdb rxdb rxjs
bun add @web-ts-toolkit/mongoose-rxdb rxdb rxjs
For production-grade local SQLite storage, also install RxDB Premium (licensed; needs an access token at install time):
- npm
- Yarn
- pnpm
- Bun
npm install rxdb-premium
yarn add rxdb-premium
pnpm add rxdb-premium
bun add rxdb-premium
If node:sqlite is unavailable in your Node runtime and you want the RxDB trial SQLite backend,
install npm sqlite3 as an optional fallback:
- npm
- Yarn
- pnpm
- Bun
npm install sqlite3
yarn add sqlite3
pnpm add sqlite3
bun add sqlite3
No sqlite3 install is required on Node 22+: the built-in node:sqlite module is
auto-detected and used by the free trial SQLite storage (it writes a real file but
is capped at ~500 docs/collection, has no indexes, and prints a warning each load).
This package supports Node 22+. sqlite3 is only a Node fallback for runtimes where
node:sqlite cannot be opened; non-Node runtimes must provide their own RxDB factory.
For real production SQLite, install rxdb-premium.
Peer dependencies:
rxdb >=17.4.0 <18(required)rxjs >=7.8.0 <8(required)rxdb-premium >=17.4.0 <18(optional — only for the production-grade SQLite storage)sqlite3 >=5 <6(optional — only for the trial SQLite path in Node runtimes withoutnode:sqlite)
Imports And Module Identity
Use named imports as the canonical style:
import { Connection, Schema } from '@web-ts-toolkit/mongoose-rxdb';
import { createMemoryDatabase } from '@web-ts-toolkit/mongoose-rxdb/storage';
Default exports are retained only as redundant compatibility conveniences. Prefer named imports in new code because they make the public API clearer to TypeScript, editors, and bundlers.
The package publishes separate ESM and CommonJS builds. If one process loads both formats, each format
has its own Schema/Connection class identity and its own defaultConnection; there is no supported
cross-format singleton. Pick one module format per application graph, and pass explicit Connection
instances across boundaries when integration code might mix ESM and CommonJS.
Compatibility Matrix
| Runtime | RxDB | RxJS | Evidence |
|---|---|---|---|
| Node 22+ | >=17.4.0 <18 | >=7.8.0 <8 | Package tests, strict NodeNext/Bundler declaration consumers, packed pnpm/npm runtime imports, and packed README quickstart run against the workspace dev dependencies (rxdb ^17.4.0, rxjs ^7.8.2). |
Future RxDB or RxJS majors are intentionally outside the peer range until they have the same package, declaration, and packed-consumer coverage.
What It Exposes
From the root entrypoint:
Schema— type paths, defaults,required/enum/min/max/match/validate, methods, statics, virtuals,pre/post,plugin,cloneDocument— change-tracked instances withisModified,modifiedPaths,markModified,validate,save,remove,toObject,toJSON,get/setValidationError— thrown byvalidate()andsave()for schema violationsQuery— thenable chainable builder (where,equals,gt/gte/lt/lte/ne,in/nin,exists,regex,or/and/nor,limit,skip,sort,select,lean,exec)Model—find,findOne,findById,create,insertMany,updateOne,updateMany,deleteOne,deleteMany,findOneAndUpdate,findOneAndDelete,countDocuments, plus schemastaticsConnection— RxDB-backed connection withconnect,model,modelNames,deleteModel,disconnectdefaultConnection,connect(...),disconnect(...),model(...)— convenience accessors over a shared default connectionMiddlewareEngine— kareem-like async pre/post engine- Converters:
convertToRxJsonSchema,castDocumentToSchema,castValue - Query compiler helpers:
translateFilter,applyUpdate,compileQuery RxCollectionAdapter— thinRxLikeCollectionover a realRxCollection
From the @web-ts-toolkit/mongoose-rxdb/storage subpath:
-
createMemoryDatabase(opts?)— in-process memory storage (tests and quick prototyping) -
createSqliteDatabase(opts?)— local SQLite. Resolution order is automatic, but a requested SQLite database fails closed when no backend can be opened:rxdb-premium'sgetRxStorageSqlite(production-grade; needs a license token at install).- RxDB's free trial
getRxStorageSQLiteTrialdriven by Node 22+'s built-innode:sqlite— persists to files derived fromopts.filePath, prints a warning each load, capped at ~500 docs/collection, no indexes. - Same trial with npm
sqlite3in Node, if installed. - In-memory
getRxStorageMemoryonly when you passallowMemoryFallback: true.
This is a breaking safety change from older releases:
createSqliteDatabase({ filePath })no longer silently creates volatile memory storage when SQLite is unavailable. It rejects withSqliteStorageError, whosecausesarray preserves backend-specific load/open failures.filePathis exact for Premium (sqliteDatabasePath) and adatabaseNamePrefixfor trial backends (which append a_trial_<databaseName>suffix, so on-disk names differ from the requested path).filePathdefaults to':memory:', which is volatile-only: it selects genuine in-memory storage whenallowMemoryFallback: trueis passed and is rejected otherwise, because trial SQLite backends would open an ordinary relative file such as:memory:_trial_<databaseName>instead of SQLite's special in-memory name. Only the memory backend reportspersistent: false; every SQLite backend reportspersistent: true. The returned database exposessqliteBackendandsqliteStorageInfo.On success a one-line
[mongoose-rxdb] createSqliteDatabase: using <backend> SQLite at <path>warning is printed (with the trial caveat for tiers 2 and 3). For real production SQLite, installrxdb-premium.
Quick Start
import { Connection, Schema, type HookNext, type HydratedDocument } from '@web-ts-toolkit/mongoose-rxdb';
import { createMemoryDatabase } from '@web-ts-toolkit/mongoose-rxdb/storage';
interface User {
name: string;
age: number;
role: 'admin' | 'user';
tags: string[];
}
interface UserMethods {
addTag(tag: string): string[];
}
interface UserVirtuals {
isAdmin: boolean;
}
type UserDocument = HydratedDocument<User, UserMethods, UserVirtuals>;
const conn = new Connection();
await conn.connect(() => createMemoryDatabase({ name: 'quickstart' }));
const userSchema = new Schema<User, UserMethods, {}, UserVirtuals>({
name: { type: String, required: true },
age: { type: Number, default: 0, min: 0, max: 150 },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
tags: [String],
});
userSchema.pre('save', function (this: UserDocument, next: HookNext) {
console.log('about to save', this.name);
next();
});
userSchema.virtual('isAdmin').get(function (this: UserDocument) {
return this.role === 'admin';
});
userSchema.method('addTag', function (this: UserDocument, tag: string) {
this.tags.push(tag);
return this.tags;
});
const User = conn.model('User', userSchema);
const ada = await User.create({ name: 'Ada', age: 36, role: 'admin', tags: [] });
console.log(ada.isAdmin); // true
ada.addTag('math');
const admins = await User.find({ role: 'admin' }).sort({ age: 1 });
await User.updateOne({ name: 'Ada' }, { $inc: { age: 1 } });
await User.deleteOne({ name: 'Ada' });
console.log(admins.map((user) => user.name));
await conn.disconnect();
For durable local storage, replace the memory factory with createSqliteDatabase({ filePath: './app.db' }).
That request fails closed unless Premium, Node 22 node:sqlite, or npm sqlite3 can be opened; pass
allowMemoryFallback: true only when volatile storage is acceptable. Custom RxDB factories must register
RxDBQueryBuilderPlugin before creating the database because query sorting and limiting rely on it.
TypeScript
Use Schema<RawDoc, Methods, Statics, Virtuals> as the source of truth. Connection#model() infers the model from that schema, including raw fields, instance methods, statics, and virtuals, so strict consumers do not need broad casts.
RawDocument<T>andLeanResult<T>expose only domain fields plus_id; RxDB metadata fields (_rev,_meta,_attachments,_deleted) are not public result types.- Hydrated operations return
HydratedDocument<T, Methods, Virtuals>, which combinesDocument<T>, raw fields, methods, and virtual properties. Query<Result>implementsPromiseLike<Result>, soawait User.find()andawait User.findOne()preserve exact result types..catch()and.finally()return typed promises..lean(true)changes document-producing results toLeanResult<T>records without document methods;.lean(false)restores the hydrated type.UpdateResult,DeleteResult, andcountDocuments()numbers are preserved unchanged, and nullable document results preservenull.findOneAndUpdate(..., { lean: true })andfindOneAndDelete(..., { lean: true })returnLeanResult<T> | null.- Projected lean records remain typed as the full
LeanResult<T>; projection does not narrow the type to a partial. - Intentionally public thrown errors (
WriteNormalizationError,MutationPartialFailureError,BulkWritePartialFailureError) are importable from the package root forinstanceofnarrowing; deep imports are not required. FilterQuery<T>rejects misspelled fields and incompatible operators. UseLooseFilterQuery<T>only as an explicit untrusted-input boundary beforesanitizeFilter().UpdateQuery<T>is field-kind aware:$inc/$mulrequire numeric fields, array operators require array fields and element values, and_id/RxDB metadata are excluded from updates.validateSync()is synchronous and returnsValidationError | undefined; use asyncvalidate()when middleware or async validators must run.
Schema
Schema follows the Mongoose shape: { field: Type } or { field: { type, ...opts } }.
const schema = new Schema({
name: { type: String, required: true, match: /^[A-Z]/ },
age: { type: Number, default: 18, min: 0, max: 150 },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
tags: [String],
meta: { type: Object },
});
Supported SchemaTypeOptions:
type—String|Number|Boolean|Date|Object| nestedSchema|[ItemType]required—boolean,[boolean, string], or a function (including[fn, message]). Function-valuedrequiredis evaluated dynamically by validation and is never emitted as an unconditional entry in public JSON Schema or RxDBrequiredlists.default— a value or a zero-arg function returning a valueenum,min,max,matchvalidate— a function or{ validator, message }immutableindex— a storage-dependent lookup hint, not a uniqueness guarantee
Supported schema-level options are _id, collection, and validateBeforeSave. Unsupported
Mongoose options fail early with SchemaConfigurationError instead of being ignored, including
timestamps, versionKey, path get / set, alias, select, ref, auto, sparse, expires,
and unique. unique is not a backend-safe constraint in this package; use index: true only as a
lookup hint and enforce uniqueness in a layer that can provide an atomic guarantee.
Schema structure is compiled into a model snapshot. After connection.model(name, schema) returns,
structural schema.add() calls are rejected, and direct mutations to the original schema's path maps
cannot change that model's casting, validation, public JSON Schema, or RxDB schema. schema.clone()
creates an independent editable copy, including independent paths, child schemas, hooks, virtuals,
options, and query helpers.
Nested structure requires an explicit child Schema ({ profile: childSchema },
{ profile: { type: childSchema } }, [childSchema] for subdocument arrays). Inline nested
plain-object definitions ({ profile: { name: String } }), dotted path names, and prefixed
schema.add(obj, prefix) are rejected with SchemaConfigurationError before collection creation;
full Mongoose nested syntax is intentionally not supported.
Helpers:
schema.method('fullName', function () {
return this.name;
});
schema.method({
greet() {
return 'hi';
},
});
schema.static('byName', function (name: string) {
return this.findOne({ name });
});
schema.virtual('isAdmin').get(function () {
return this.role === 'admin';
});
schema.pre('save', function (next) {
/* ... */ next();
});
schema.post('save', function () {
/* ... */
});
schema.plugin((s) => {
/* mutate s */
});
schema.clone();
Document
Instances track modifications:
const doc = new User({ name: 'Grace' });
doc.isModified('name'); // true
doc.name = 'Grace Hopper';
doc.isModified('name'); // true
doc.modifiedPaths(); // ['name']
await doc.save();
doc.isModified('name'); // false
doc.toObject({ virtuals: true });
doc.toJSON();
Document exposes:
isModified(path?),modifiedPaths(),markModified(path),clearModified()validate(),save(),remove()/deleteOne()toObject(opts?),toJSON()get(path),set(path, value)(orset({ ...values }))- schema
methodsbound as instance methods - schema
virtualsas getter/setter properties
Loaded documents keep a deep snapshot of the last persisted state. Top-level assignment marks paths
explicitly, and supported mutable values are also detected by structural diffing when save() runs:
arrays, plain objects, nested subdocuments, JSON-like mixed values, and Date instances. Mutating
doc.tags, doc.profile.score, or a date instance on the document can therefore persist without an
explicit setter call.
Constructor input and toObject() / toJSON() results are cloned at the boundary. Mutating an input
object or a plain object returned by toObject() cannot mutate the live document or mark it dirty.
markModified(path) is reconciled with the snapshot. It remains useful for supported mixed values, but
unchanged and reverted paths are treated as clean. Saving an unchanged loaded document skips adapter
mutation. The snapshot is refreshed only after successful persistence; failed saves keep their modified
paths for retry.
Query
Model.find() returns a thenable chainable Query. Execution is deferred until .exec(),
.then() (i.e. await), .catch(), or .finally() is called.
// chainable
await User.find().where('age').gt(18).limit(10).sort({ age: -1 }).exec();
// mango-style filter
await User.find({ role: { $in: ['admin', 'user'] }, age: { $gte: 18 } });
// awaitable
const users = await User.findOne({ name: 'Ada' });
// update / delete
await User.updateOne({ name: 'Ada' }, { $inc: { age: 1 } });
await User.deleteMany({ role: 'user' });
await User.findOneAndUpdate({ name: 'Ada' }, { $set: { age: 37 } }, { new: true });
// count
await User.countDocuments({ age: { $gte: 18 } });
Supported query operators: $gt, $gte, $lt, $lte, $ne, $in, $nin, $exists, $regex
(+$options), and top-level $and / $or / $nor.
Supported update operators: $set, $unset, $inc, $mul, $min, $max, $push, $pull,
$addToSet, plus a plain { field: value } alias for $set.
All current write routes (create, insertMany, document save, update operators,
replacement-style updates, and supported updateOne(..., { upsert: true }) /
findOneAndUpdate(..., { upsert: true })) use the same schema-aware normalization pipeline before
persistence. Values are cast by their declared schema path, and validation sees the normalized value
that will be written.
The persistence adapter boundary exposes only domain fields plus the logical _id primary key. RxDB
revision metadata (_rev, _meta, _attachments, _deleted) is stripped before records reach public
documents, lean results, update callbacks, or the fake test adapter.
Model.create() and Model.insertMany() share one insertion pipeline. create() keeps per-document
save middleware and inserts one document at a time. insertMany() runs insertMany middleware and
uses the adapter bulk-insert path. It is ordered by default: records before the first storage failure
remain inserted and a BulkWritePartialFailureError reports insertedCount, insertedIds, inserted
records, and record-level errors. Pass { ordered: false } to attempt every input record and receive
the same partial-failure shape for all failed indexes.
Dates are stored as ISO-8601 strings (Date#toISOString()) in memory and SQLite-backed storage, then
hydrated back to Date instances when documents are read. Dotted update paths such as
profile.score update nested objects structurally; literal top-level dotted keys are not written.
Dangerous path segments (__proto__, prototype, constructor), unknown update operators,
incompatible arithmetic or array operators, _id, immutable paths, and RxDB metadata (_rev, _meta,
_attachments, _deleted) are rejected before mutation.
Mutation options are intentionally narrower than full Mongoose and unsupported options throw
MutationOptionError instead of being ignored:
updateOne:sort,upsert,runValidators,setDefaultsOnInsert.updateMany:sort,runValidators; multi-upsert is not supported.deleteOne:sortonly.deleteManyaccepts no options.findOneAndUpdate:sort,upsert,new,returnDocument,runValidators,setDefaultsOnInsert,lean.findOneAndDelete:sort,lean.
runValidators: true validates the final normalized storage value before persistence for existing
updateOne, updateMany, and findOneAndUpdate matches. With validation disabled, compatible casted
updates can persist values that violate schema validators. Upsert inserts are always validated because
they create a new record.
For findOneAndUpdate, returnDocument takes precedence over new when both are present:
returnDocument: 'before' returns the previous document, while returnDocument: 'after' and
new: true return the updated or inserted document. The default is the before document; an upsert that
returns before yields null.
Upsert inserts are built from eligible top-level equality filter fields (field: value and
field: { $eq: value }) plus the normalized update. Operator predicates such as $gt are not copied
into the inserted record. _id is generated when the equality filter does not provide one.
setDefaultsOnInsert applies schema defaults only when it is exactly true, and it is rejected unless
upsert: true is also set.
Read query semantics are intentionally defined for the supported subset:
limit()andskip()must be non-negative safe integers.- Results are sorted first, then
skip()is applied beforelimit(). findOne()follows the same ordering and skip policy, then returns at most one document after the skipped window.select()supports inclusion, exclusion, string projections, and_idoverrides. Mixed inclusion/exclusion projections are rejected except for_id.- Projection is applied before hydration; defaults do not recreate projected-out fields.
lean()returns normalized plain records directly and does not constructDocumentinstances or runinithooks. Lean applies only to document-producing reads;update/delete/countresults keep their count shapes, and passingleanto those operations rejects withMutationOptionError.countDocuments()uses the adapter count path, ignoressort(), and honorsskip()/limit()by counting the paginated match window.
Query instances are single-use like Mongoose queries. The first execution through exec(), await,
.then(), .catch(), or .finally() owns the query; a second execution attempt rejects with the
package-owned MongooseError (QueryExecutionError). Clone before executing when you need another
variant. Filters, options, and updates are deep-copied at construction and clone time, and execution uses
a snapshot taken before query middleware runs.
Middleware
A kareem-like engine runs async pre and post hooks. Hooks may be callback-style
(function (next) { ...; next(); }) or promise-style (async function () { ... }).
schema.pre('save', function (next) {
if (this.name === 'banned') return next(new Error('not allowed'));
next();
});
schema.post('save', function () {
metrics.increment('user.save');
});
Hooked operations: save, remove, validate, updateOne, updateMany, deleteOne,
deleteMany, findOne, find, findOneAndUpdate, findOneAndDelete, insertMany, init.
Retained middleware behavior is intentionally narrower than full Mongoose:
- Document hooks (
validate,save,remove, documentdeleteOne,init) run withthisset to the document. - Query hooks run with
thisset to theQueryinstance; inspect state withgetFilter(),getOptions(), andgetUpdate(). insertManyhooks run withthisset to the model. Promise-stylepre('insertMany', function (docs) {})receives the input docs; callback-style receives(next, docs).- Post success hooks receive
(result)or callback-style(result, next). - Error post hooks must be registered with
{ errorHandler: true }and receive(err)or callback-style(err, next). - Callback-style middleware that also returns a promise settles once; whichever callback or promise settles first wins.
- The TypeScript hook-name surface is limited to the listed operations; unsupported Mongoose hook names are not claimed.
Validation recurses through nested Schema paths and arrays of subdocuments. Failures are aggregated
into one ValidationError whose errors map is keyed by full logical paths such as profile.name or
members.0.role. Conditional required functions and custom validators run with this bound to the
owning document for root paths, or to the plain subdocument object for nested schema paths and
subdocument-array items. save() runs validate() by default; { validateBeforeSave: false } skips
automatic save validation while leaving explicit doc.validate() unchanged.
validateSync() performs schema validation synchronously without middleware. Async custom validators
produce a sync ValidationError for that path; call validate() to run async validators and validation
middleware.
Connection & Storage
Connection wraps an RxDB database. Pass any async factory that returns a Promise<RxDatabase>.
Connection strings are not supported and are rejected before storage creation; a URL is never treated
as an in-memory request.
import { createMemoryDatabase } from '@web-ts-toolkit/mongoose-rxdb/storage';
const conn = new Connection();
await conn.connect(() => createMemoryDatabase({ name: 'myapp' }));
Storage subpath helpers:
createMemoryDatabase({ name? })— fast in-process storage, default for testscreateSqliteDatabase({ name?, filePath?, allowMemoryFallback? })— local SQLite resolved automaticallyrxdb-premium(production-grade; needs a license token at install)- RxDB free trial
getRxStorageSQLiteTrialdriven by Node 22+'s built-innode:sqlite(persists to files derived fromfilePath, but capped at ~500 docs/collection, no indexes, prints a warning each load) - Same trial with npm
sqlite3in Node, if installed - In-memory
getRxStorageMemoryonly whenallowMemoryFallback: trueis passed
Persistent requests fail closed by default. If no SQLite backend can be opened,
createSqliteDatabase({ filePath }) rejects with SqliteStorageError and does not create a memory
database. Inspect error.causes for backend-specific load/open failures, or inspect
db.sqliteStorageInfo after a successful connection for the selected backend and path semantics.
filePath is exact for Premium and a databaseNamePrefix for RxDB trial backends
(which append a _trial_<databaseName> suffix). It defaults to ':memory:', which is
volatile-only: genuine in-memory storage with allowMemoryFallback: true, rejected otherwise.
A shared default connection is also available for simple apps:
import { connect, model, Schema, disconnect } from '@web-ts-toolkit/mongoose-rxdb';
import { createSqliteDatabase } from '@web-ts-toolkit/mongoose-rxdb/storage';
await connect(() => createSqliteDatabase({ filePath: './app.db' }));
const User = model('User', new Schema({ name: String }));
await disconnect();
Connection state is explicit: disconnected, connecting, connected, closing, or failed.
Concurrent connect() calls share one in-flight connection attempt, concurrent disconnect() calls
share one close operation, and calling connect() while already connected rejects. To switch storage,
call disconnect(), then compile fresh models on the reconnected Connection; model objects from the
previous connection are invalidated and must not be reused.
Collections are registered by normalized lower-case collection name. Equivalent schemas targeting the
same normalized collection share one collection initialization and adapter. Incompatible schemas for
the same normalized name, including case-only collection-name collisions, throw before storage is
touched. If collection initialization fails, the failed model is removed from connection.modelNames()
and can be retried with the same model name after fixing the cause.
Security: sanitizeFilter
Filters built from user input can leak Mango operators ($where, $func, ...). Call
sanitizeFilter at the request boundary before passing untrusted filters to model methods. It is
caller-invoked, not automatic request parsing. Query execution also validates filters and rejects
unsupported operators if a caller bypasses sanitization.
import { QueryFilterError, sanitizeFilter } from '@web-ts-toolkit/mongoose-rxdb';
try {
const safe = sanitizeFilter(req.body.filter);
await User.deleteMany(safe);
} catch (error) {
if (error instanceof QueryFilterError) {
// The rejected filter was not executed, so unrelated documents were not touched.
}
}
Only object filters using the logical operators $and, $or, $nor (recursed into) and the Mango per-field operators
($eq, $gt, $gte, $lt, $lte, $ne, $in, $nin, $exists, $regex, $options) pass
through. null and other non-object filters, invalid top-level operators, unsupported field operators, malformed logical arrays,
dangerous keys (__proto__, prototype, constructor), excessive nesting, and excessive logical
array width throw QueryFilterError; rejected filters are never broadened to {}.
Regex filters are allowed only under a strict bounded policy before adapter execution: pattern text
must be at most 128 characters, flags may only be i, m, s, or u, and duplicate/invalid flags,
backreferences, lookaround, repeated wildcard scans, quantified alternation, and nested quantified
groups such as ^(a+)+$ are rejected.
_id
Each document auto-generates a _id — a UUIDv4 when globalThis.crypto.randomUUID is available,
otherwise a short random+timestamp string. You may pass an explicit _id in the constructor data
or Model.create(data). After construction _id is read-only (no setter): RxDB primary keys
cannot be changed after insert, so the field is immutable.
Connection model registration
Connection#model(name, schema, collection?, options?) compiles a schema into a Model. Calling it
twice with the same name and a new schema throws (matching Mongoose's OverwriteModelError)
unless you pass { overwrite: true }. To register a different shape, call
connection.deleteModel(name) first, or use { overwrite: true }. This only replaces the model
registration. The underlying RxDB collection schema is not migrated by delete/overwrite, so use a
distinct collection name or perform an explicit migration outside this package before changing
persisted collection shape.
How It Maps to RxDB
| Mongoose concept | Implementation in this package |
|---|---|
| Schema definition | Schema → convertToRxJsonSchema (Draft-07 RxJsonSchema) |
| Casting & validation | castDocumentToSchema + Document.validate() (schema-level rules) |
Middleware (pre/post) | MiddlewareEngine, mapped onto Model/Query/Document ops |
| Document methods | Schema.methods, attached to hydrated Document instances |
| Statics | Schema.statics, attached to the compiled Model |
| Virtuals | Schema.virtual(...) getters/setters on Document |
| Query builder | Query → compileQuery → RxDB Mango query via RxCollectionAdapter |
| Dirty tracking | Document.isModified / modifiedPaths, $set-only diffs on save |
| Storage | Connection + createSqliteDatabase / createMemoryDatabase |
Current Scope
This package is a core MVP proxy. Out of scope for now:
populate(virtual and path population)aggregate/ pipeline cursors- index declaration sync (
syncIndexes) - sessions / transactions
- discriminators
bulkWrite/bulkSave- streaming
QueryCursor
These can be layered on as the design doc's four pillars (schema, document, middleware, query) are extended. The internal split is intentionally modular so each missing piece slots in without reworking the others.
When To Use It
Use @web-ts-toolkit/mongoose-rxdb when you want:
- Mongoose-shaped code (schemas, models, queries, hooks) but persisted locally
- offline-first storage backed by SQLite via RxDB
- a storage-agnostic API that reads like Mongoose and swaps backends via a factory
If you need full Mongoose parity (populate, aggregate, MongoDB driver), use mongoose
directly against MongoDB; this package targets the local/offline subset.