@push.rocks/smartdata
v11.4.0
Published
An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.
Maintainers
Readme
@push.rocks/smartdata 🚀
The ultimate TypeScript-first MongoDB ODM — type-safe decorators, real-time change streams, Lucene-powered search, distributed leader election, and cursor streaming. Built for modern applications that demand performance, correctness, and developer experience.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
🌟 Why SmartData?
- 🔒 100% Type-Safe — TC39 Stage 3 decorators, generic filters, and compile-time query validation
- ⚡ High Performance — Connection pooling, cursor streaming, and automatic indexing
- 🔄 Real-time Ready — MongoDB Change Streams with RxJS for reactive applications
- 🌍 Distributed Systems — Built-in leader election and task coordination via
@push.rocks/taskbuffer - 🛡️ Security First —
$whereinjection prevention, operator allow-listing, and input sanitization - 🔎 Lucene Search — Full-text, wildcard, boolean, and range queries out of the box
- 🎯 Great DX — Intuitive API, IntelliSense that just works, and lifecycle hooks
📦 Installation
pnpm add @push.rocks/smartdata🚦 Requirements
- Node.js >= 20.x
- Deno >= 2.0 (for Deno projects)
- MongoDB >= 5.0
- TypeScript >= 5.2 (for TC39 decorator support)
Note: SmartData uses TC39 Stage 3 decorators (the standard). Make sure
experimentalDecoratorsis not set in your tsconfig.json. Bun is not currently supported as it doesn't implement TC39 decorators yet.
🎯 Quick Start
1️⃣ Connect to Your Database
import { SmartdataDb } from '@push.rocks/smartdata';
const db = new SmartdataDb({
mongoDbUrl: 'mongodb://localhost:27017/myapp',
mongoDbName: 'myapp',
mongoDbUser: 'username',
mongoDbPass: 'password',
});
await db.init();
console.log(db.status); // 'connected'2️⃣ Define Your Data Models
import {
SmartDataDbDoc,
SmartdataPersistenceError,
type TSmartdataPersistenceErrorCode,
Collection,
compoundIndex,
unI,
svDb,
index,
searchable,
} from '@push.rocks/smartdata';
@compoundIndex({
name: 'status_createdAt',
key: { status: 1, createdAt: -1 },
})
@Collection(() => db, { collectionName: 'app_users' })
class User extends SmartDataDbDoc<User, User> {
@unI()
public id!: string;
@svDb()
@searchable()
public username!: string;
@svDb()
@searchable()
@index({ unique: false })
public email!: string;
@svDb()
public status!: 'active' | 'inactive' | 'pending';
@svDb()
public tags!: string[];
@svDb()
public createdAt: Date = new Date();
@svDb()
public updatedAt: Date = new Date();
@svDb()
public revision = 0;
@svDb()
public pendingReason?: string;
@svDb()
public preferences!: Record<string, unknown>;
constructor(username: string, email: string) {
super();
this.username = username;
this.email = email;
}
}collectionName keeps a persisted collection stable when a model class is
renamed. Within one SmartdataDb, different model constructors may bind the
same collection only when their normalized persisted schemas are equivalent.
Binding a divergent schema throws SmartdataPersistenceError with
code === 'invalid_configuration'.
Model.getIndexInfo() returns a sanitized index inventory containing only the
index name, ordered field paths and directions, uniqueness, sparsity, and
optional TTL seconds. It does not expose the MongoDB collection or raw driver
metadata.
3️⃣ CRUD Operations
// ✨ Create
const user = new User('johndoe', '[email protected]');
user.id = 'user-1';
user.status = 'active';
user.tags = ['developer', 'typescript'];
try {
const insertResult = await User.insert(user);
console.log(insertResult.insertedId);
} catch (error) {
if (
error instanceof SmartdataPersistenceError &&
error.code === 'unique_conflict'
) {
const code: TSmartdataPersistenceErrorCode = error.code;
console.error(code, error.cause);
} else {
throw error;
}
}
// 🔍 Read — fully type-safe filters
const foundUser = await User.getInstance({ username: 'johndoe' });
const activeUsers = await User.getInstances({ status: 'active' });
if (!foundUser) {
throw new Error('User not found.');
}
// ✏️ Update
foundUser.email = '[email protected]';
await foundUser.save();
// 🗑️ Delete
await foundUser.delete();Programmatic Collection Models
Use defineCollectionModel() when a model's persisted contract is assembled
without field decorators. The collection name is mandatory, so renaming a
constructor cannot silently move its data. Fields and stable named indexes are
validated before any database work; index directions may be 1, -1, or
'text'.
import {
SmartDataDbDoc,
defineCollectionModel,
} from '@push.rocks/smartdata';
class Job extends SmartDataDbDoc<Job, Job> {
public id!: string;
public state!: 'pending' | 'active';
public revision!: number;
}
defineCollectionModel(Job, () => db, {
collectionName: 'jobs',
persistedFields: ['id', 'state', 'revision'],
identityFields: ['id'],
indexes: [
{
name: 'job_id',
key: { id: 1 },
options: { unique: true },
},
{
name: 'job_state_revision',
key: { state: 1, revision: -1 },
},
],
});
await Job.ensureInitialized();Equivalent schemas may bind different constructors to the same
(SmartdataDb, collectionName) pair. A divergent schema fails with
SmartdataPersistenceError code invalid_configuration.
Every programmatic identityFields entry must also have an explicit,
single-field ascending index with options.unique === true. SmartData rejects
the model configuration synchronously when that matching index is absent.
Programmatic identityFields and plain @unI() declarations accept only
non-empty string values. A decorator model that owns an existing positive
integer identity contract can opt in explicitly:
@unI({ valueType: 'positiveSafeInteger' })
public id!: number;This opt-in accepts only finite integers from 1 through
Number.MAX_SAFE_INTEGER. It rejects strings, zero, negative values,
fractions, non-finite numbers, and unsafe integers before ordinary persistence,
strict atomic identity filters, or instance-owned identity selectors reach
MongoDB. SmartData keeps string and numeric identity domains distinct, and
collection bindings with divergent identity value types are rejected.
Collection Initialization and Topology Inspection
Model.ensureInitialized() is the mutating path. It resolves the model's bound
database, creates an absent collection, creates model-owned indexes, and verifies
the installed index definitions. Use it when the application owns database
bootstrap.
Topology inspection is read-only and does not resolve Model.collection, a
manager or default manager, or the database resolver passed to @Collection(),
@managed(), or defineCollectionModel(). It does not register a
SmartdataCollection, create a namespace or index, or run collection
initialization:
const expected = Job.getExpectedCollectionTopology();
const inspection = await Job.inspectCollectionTopology(db);
console.log(expected.collectionName, expected.indexes);
console.log(inspection.status, inspection.reasonCodes);getExpectedCollectionTopology() is synchronous and pure. Its deeply frozen
result contains the collection name and canonical _id_ plus model-owned
indexes. Each index exposes only its stable name, ordered keys, text weights,
unique, sparse, and TTL seconds. Deprecated background declarations are
normalized away. Extended expected index options that cannot be compared safely
are rejected as invalid_configuration.
inspectCollectionTopology(db) requires an explicitly supplied, connected
SmartdataDb. It snapshots the active client, database, and lifecycle
generation before issuing listCollections and listIndexes; a read failure or
connection change throws a sanitized SmartdataPersistenceError without a raw
driver error. The bounded result contains canonical expected and actual indexes
plus reason codes, never collection metadata or driver result objects.
The inspection statuses are:
absent: the namespace does not exist.compatible: an ordinary option-free collection has the canonical_id_index and its installed indexes are a valid subset of the expected indexes.exact: every expected index is installed and there are no extras.divergent: the namespace is a view or special collection, an index is changed or unexpected, or metadata uses unsupported options such as partial filters, collation, hidden, wildcard, or storage-engine options.
Model.insert() accepts only a newly constructed instance of that model and
returns MongoDB's typed InsertOneResult. On success, SmartData marks the
instance as database-backed; inserting it again is rejected. The optional
{ session } argument accepts either a raw MongoDB session or the opaque
SmartdataSession returned by SmartdataDb.createSession(). While either
session is in a transaction, the instance remains creationStatus: 'new'
because SmartData cannot observe whether the caller later commits or aborts.
Model.insertMany() inserts a non-empty batch of newly constructed instances
with the same per-document validation and returns MongoDB's typed
InsertManyResult. Each document inserts atomically; the batch as a whole is
not isolated unless the caller supplies a transaction session. With ordered
semantics (the default) MongoDB stops at the first failure; with
{ ordered: false } it attempts every document. Outside a transaction SmartData
marks exactly the documents MongoDB reports as inserted database-backed — even
when the batch fails part-way with a unique_conflict — so callers can
compensate precisely. While the supplied session is in a transaction every
instance remains creationStatus: 'new', for the same reason insert() leaves
it unchanged. Repeating the same instance in one batch is rejected.
Atomic Ordinary-Model Writes
Use the model-owned atomic API for optimistic concurrency, counters, targeted
absence, and insert-only defaults. Atomic filters and update paths name
declared top-level persisted fields, whether declared with decorators or
defineCollectionModel(). Decorator models may opt exact nested paths into
atomic selectors and updates with @svDb({ atomicPaths: [...] }); every other
dotted path and nested object selector remains rejected. An explicit
{ $eq: wholeObject } condition supports safe exact whole-object
compare-and-set without interpreting nested keys as query operators. SmartData
captures an inert snapshot before any asynchronous work; the value must be safe
BSON without accessors or custom encoders and match the stored document
exactly. Whole declared objects may still be replaced through $set.
SmartData validates supported field operators and declared identity-field
values, requires at least one equality anchor, and rejects empty updates,
direct undefined, non-finite increments, reserved fields, serialized roots,
and conflicting paths before calling MongoDB. Fields declared with plain
@unI() or programmatic identityFields use non-empty string selector values;
@unI({ valueType: 'positiveSafeInteger' }) fields require positive safe
integers. Identity fields remain immutable; create their value with insert()
or an upsert selector rather than an update operator. Ordinary unique indexes
enforce database uniqueness without adding identity-field immutability or
identity-value selector semantics.
const outcome = await User.atomicUpdate(
{ id: 'user-1', revision: 7 },
{
$set: {
status: 'active',
updatedAt: new Date(),
},
$unset: {
pendingReason: true,
},
$inc: {
revision: 1,
},
},
);
if (outcome.matchedCount !== 1) {
throw new Error('The user changed concurrently.');
}
const objectCas = await User.atomicUpdate(
{
id: 'user-1',
preferences: {
$eq: persistedPreferences,
},
},
{
$set: {
preferences: nextPreferences,
},
},
);Declare only the exact nested paths the model owns:
@Collection(() => db, { collectionName: 'services' })
class Service extends SmartDataDbDoc<Service, Service> {
@unI()
public id!: string;
@svDb({
atomicPaths: [
'reconciliationStatus',
'imageRolloutStatus',
],
})
public data!: {
reconciliationStatus: {
status: string;
generation: number;
};
imageRolloutStatus: {
rolloutId: string;
status: string;
};
};
}
const reconciliation = await Service.atomicUpdate(
{
id: 'service-1',
'data.reconciliationStatus': {
$eq: {
status: 'pending',
generation: 4,
},
},
},
{
$set: {
'data.reconciliationStatus': {
status: 'ready',
generation: 4,
},
},
},
);The authoritative result contains acknowledged, matchedCount,
modifiedCount, and upsertedId. A successful no-op can therefore report one
match and zero modifications, while an upsert reports its new identifier.
Options expose only upsert and a caller-owned MongoDB session.
SmartData maintains _updatedAt on every atomic write and _createdAt on
upsert.
Use atomicFindOneAndUpdate() when the caller needs an atomic before or after
image rather than a separate reload. The required returnDocument option is
explicit, and the result contains a sanitized matched, upserted, or
not_matched status. Every non-null returned document is hydrated as the model;
MongoDB command metadata and driver result objects are never exposed.
const transition = await User.atomicFindOneAndUpdate(
{ id: 'user-1', revision: 7 },
{
$set: { status: 'active' },
$inc: { revision: 1 },
},
{ returnDocument: 'after' },
);
if (transition.status !== 'matched') {
throw new Error('The user changed concurrently.');
}
console.log(transition.document.revision); // 8The options object accepts only returnDocument, optional upsert, an
optional declared-top-level-field sort, and an optional raw or
SmartData-owned session. It does not accept projection, hint, array filters,
arbitrary operators, or bulk behavior. The sort option orders the candidate
documents before MongoDB selects the single document to update — the
claim-oldest-work pattern — and callers wanting deterministic ordering should
include a unique tiebreaker field. When an upsert
inserts a document, returnDocument: 'before' reports upserted with a null
document, while returnDocument: 'after' returns the hydrated inserted model.
An upsert that matches an existing document reports matched. SmartData fails
closed when MongoDB does not provide a recognized authoritative result shape.
Use atomicDelete() when deletion itself must be compare-and-set fenced. In
addition to the ordinary strict-filter rules, the selector must contain
equality anchors that fully pin one globally constraining key set: a declared
@unI() or programmatic identityFields field on its own, or every field of
one declared unqualified unique index. An index qualifies only when it declares
no option other than unique and background and every key direction is 1
or -1; sparse, partial, TTL, and text unique indexes never qualify because
they do not guarantee at most one equality match. Anchors may be spread across
the top level and $and branches; equalities that appear only inside
alternative $or branches are rejected, so deleteOne never chooses an
arbitrary match.
const deletion = await User.atomicDelete({
id: 'user-1',
revision: 8,
});
if (deletion.deletedCount !== 1) {
throw new Error('The user changed concurrently.');
}The static operation returns acknowledged and deletedCount. It does not
hydrate a model or run instance deletion hooks; use instance.delete() when
those hooks are required. The optional { session } participates in a
caller-owned transaction.
atomicUpdate(), atomicFindOneAndUpdate(), and atomicUpdateMany() also
support single-element array operators on declared
top-level array fields: $push appends one element (duplicates allowed),
$addToSet adds one element unless an equal element already exists, and
$pull removes every element equal to a scalar operand. $push and
$addToSet elements pass the same inert-value validation as $set, so plain
object elements are allowed while modifier documents ($each, $position,
$slice, $sort) are rejected. $pull accepts only scalar operands — strings,
finite numbers, booleans, null, dates, binary values, and other trusted BSON
scalars — and rejects regular expressions, operator documents, arrays, and plain
objects. Nested paths, serialized fields, and identity fields are rejected for
all three operators.
await CacheEntry.atomicUpdate(
{ org: 'foss', key: 'bundle-1' },
{
$addToSet: { leaseIds: leaseId },
$set: { state: 'ready' },
},
);
await CacheEntry.atomicUpdate(
{ org: 'foss', key: 'bundle-1' },
{ $pull: { leaseIds: leaseId } },
);Use atomicDeleteMany() and atomicUpdateMany() for plural writes behind the
same strict declared-field validation. Because plural intent is explicit in
these APIs, the filter does not need to pin a unique index, but it still
requires at least one equality anchor — at the top level, inside an $and
branch, or in every $or branch — so range-only and empty filters are
rejected. Each matched document is deleted or updated atomically; the batch as
a whole is not isolated unless the caller supplies a transaction session.
atomicUpdateMany() cannot upsert and
rejects $setOnInsert; it maintains _updatedAt on every matched document
and returns acknowledged, matchedCount, and modifiedCount.
const purged = await Delivery.atomicDeleteMany({
org: 'foss',
webhookId: 'hook-1',
state: { $in: ['pending', 'delivering'] },
});
const abandoned = await MaintenanceRun.atomicUpdateMany(
{ state: 'running' },
{ $set: { state: 'failed' } },
);Use getGroupedTotals() for bounded server-side grouped counts and numeric
sums — the quota-accounting shape — without raw aggregation access. It groups
by one or two declared, non-serialized top-level fields, always returns a
count per group, and requires an explicit limit (1..10000). The group
stage is not sorted, so when more groups exist than the limit the returned
subset is arbitrary; detect overflow with the limit-plus-one idiom. Documents
missing a group field merge with explicit-null values into one group reported
as null. An optional filter uses the ordinary read-filter semantics, and
the options also accept an optional maxTimeMS and a caller-owned session.
sums names declared, non-serialized fields that TypeScript types as
numeric; that numeric restriction is type-level, so an untyped caller naming
a non-numeric declared field receives 0 for it instead of an error. A total
that is not a finite JavaScript number — a Decimal128 field, for example —
fails closed with SmartdataPersistenceError code unsupported_operation,
and exact-persistence models reject the API with that same code; unsupported
option keys and out-of-range bounds throw invalid_argument.
const retained = await PackageVersion.getGroupedTotals({
groupBy: ['owner'],
sums: ['sizeBytes'],
limit: maximumInventoryOwners + 1,
});
if (retained.length > maximumInventoryOwners) {
throw new Error('Owner inventory exceeds the supported limit.');
}Instance-level save(), delete(), and updateFromDb() target the document
through its declared identity fields. On a model that declares none, the update
branch of save(), plus delete() and updateFromDb(), fail closed with
SmartdataPersistenceError code invalid_configuration instead of issuing an
empty filter that would address an arbitrary document. Such models still insert
through Model.insert(), Model.insertMany(), or a first save(), and
perform every later read-modify-write through the static atomic APIs.
$setOnInsert and { upsert: true } support race-safe registration without
resetting existing fields. Duplicate insert and upsert conflicts throw
SmartdataPersistenceError with code === 'unique_conflict' and retain the
MongoDB error as cause. Exact-persistence models reject this ordinary API and
continue to use Model.exact.
🔥 Features
Exact Persistence
Use exact persistence for immutable records or state machines where the stored BSON shape is part of the application contract. SmartData owns the storage envelope, canonical BSON projection, reconciliation, compare-and-swap transitions, and revision-fenced deletion. The application supplies one strict synchronous assertion for its document body.
import {
Collection,
SmartDataDbDoc,
TExact,
exactPersistence,
svDb,
unI,
} from '@push.rocks/smartdata';
interface IAuditDelivery {
id: string;
data: {
state: 'pending' | 'delivered';
eventId: string;
sinkId: string;
nextAttemptAt: number;
};
}
const assertAuditDelivery = (
value: unknown,
): asserts value is IAuditDelivery => {
if (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
) {
const document = value as Record<string, unknown>;
const data = document.data as Record<string, unknown> | undefined;
if (
Object.keys(document).every((key) => ['id', 'data'].includes(key)) &&
typeof document.id === 'string' &&
typeof data === 'object' &&
data !== null &&
!Array.isArray(data) &&
Object.keys(data).every((key) =>
['state', 'eventId', 'sinkId', 'nextAttemptAt'].includes(key),
) &&
typeof data.eventId === 'string' &&
typeof data.sinkId === 'string' &&
Number.isSafeInteger(data.nextAttemptAt) &&
(data.state === 'pending' || data.state === 'delivered')
) {
return;
}
}
throw new Error('Invalid audit delivery document.');
};
@Collection(() => db)
@exactPersistence({
assertDocument: assertAuditDelivery,
})
class AuditDelivery extends SmartDataDbDoc<AuditDelivery, IAuditDelivery> {
declare static exact: TExact<AuditDelivery>;
@unI() public id!: string;
@svDb() public data!: IAuditDelivery['data'];
}
const inserted = await AuditDelivery.exact.insert({
id: 'delivery-123',
data: {
state: 'pending',
eventId: 'event-123',
sinkId: 'lossless-object-storage',
nextAttemptAt: Date.now(),
},
});
const transitioned = await AuditDelivery.exact.transition({
current: inserted.document,
change: (delivery) => {
delivery.data = {
...delivery.data,
state: 'delivered',
};
},
});
if (transitioned.status !== 'transitioned') {
throw new Error('The audit delivery changed concurrently.');
}
const querySignal = AbortSignal.timeout(5_000);
const pending = await AuditDelivery.exact.findStored({
filter: { 'data.state': 'pending' },
sort: { 'data.nextAttemptAt': 1 },
signal: querySignal,
});
const sinkIds: string[] = await AuditDelivery.exact.distinct('data.sinkId');
const applicationBody = AuditDelivery.exact.toPersisted(transitioned.document);
const canonicalBodyBytes = AuditDelivery.exact.canonicalBytes(applicationBody);
const deleted = await AuditDelivery.exact.delete({
current: transitioned.document,
});
if (deleted.status !== 'deleted') {
throw new Error('The audit delivery changed concurrently.');
}Exact-persistence models permit only default string-valued @unI()
declarations; the presence of any
@unI({ valueType: 'positiveSafeInteger' }) field is invalid configuration,
even when another field is selected as reconcileBy. When the model has
exactly one @unI() string field, SmartData infers it as the reconciliation
identity. A model with no @unI() field is invalid. With multiple @unI()
fields, pass reconcileBy explicitly; it must name one of those persisted
string fields. Exact inserts report inserted, already_committed, or
conflict. Transitions use
SmartData-owned _smartdataRevision metadata and report transitioned or
concurrent_change; transition callbacks must be synchronous. Initial and
pre-existing revisionless documents receive their first revision on transition.
Exact deletion accepts only { current } and synchronously validates and
snapshots that stored document into inert BSON values before I/O. Accessors,
proxy traps, and custom or inherited BSON encoders are rejected without being
invoked. Malformed official BSON scalar internals fail with
SmartdataExactPersistenceError code invalid_document before any write. The
delete is atomically fenced by _id, reconciliation identity,
and its current revision. Legacy revisionless documents use an explicit
revision-absence fence. It reports { status: 'deleted', deletedCount: 1 } or
{ status: 'concurrent_change', deletedCount: 0 }.
Exact model instances do not support save() or delete(). Use
Model.exact.insert(), findStoredOne(), findStored(), count(),
distinct(), transition(), and the static Model.exact.delete() so
validation, reconciliation, and concurrency guarantees stay intact. Exact
delete never hydrates a model or invokes instance hooks, never upserts, and
does not accept a caller-authored selector. SmartData rejects undeclared
top-level fields and the reserved _id, _smartdataRevision, _createdAt,
and _updatedAt fields. The assertion owns nested and cross-field domain
rules and must be strict, synchronous, and free of mutations.
Every exact database operation accepts a SmartData-owned session through its
options or a final { session } argument. Call Model.exact.prepare() for
every participating model before any owned-session operation, including before
opening a transaction; exact operations fail closed rather than running
collection or index initialization under a session. Programmatic models reuse their verified stable named indexes, so
exact initialization never adds a second unnamed identity index.
findStored() additionally accepts a caller-owned signal. SmartData checks an
already-aborted signal before initialization or session acquisition and forwards
the same signal to the MongoDB cursor, preserving signal.reason by identity.
Use AbortSignal.timeout() as above, or compose caller deadlines and request
cancellation with AbortSignal.any(). SmartData does not create or abort the
controller. Cancellation applies only to the finite findStored() cursor query;
it does not add cancellation to other exact APIs or interrupt collection/index
initialization already in progress. Call prepare() first when initialization
must be kept outside the query deadline. A query or canonicalization error takes
precedence over a concurrent cursor-close error; after a successful query, a
cursor-close error is returned.
Abort-triggered and explicit cursor cleanup share one completion, so an explicit
SmartData session is not reusable until the native cursor cleanup has settled.
Canonicalization sorts every plain-object level while preserving array positions
and treating BSON values atomically. toPersisted() validates a stored envelope
and returns an ordinary application object with only SmartData storage metadata
removed; nested data properties named __proto__ and BSON scalar types remain
intact. canonicalBytes() accepts only an application body, so signatures,
hashes, and external sinks do not accidentally include storage metadata. Sort
keys and distinct() fields accept type-checked dot paths through nested objects
and array elements.
Contract violations and inconclusive writes throw
SmartdataExactPersistenceError; inspect its exported
TExactPersistenceErrorCode. In particular, code === 'ambiguous_write' means
the write may have committed but SmartData could not prove the stored outcome
during reconciliation. Do not retry it with a new reconciliation identity or
treat it as a definite rollback. Keep the same identity and route the operation
to explicit retry or operator reconciliation. For exact deletion, an
unacknowledged or thrown write reports concurrent_change only when
same-identity reconciliation still finds the same _id; absence or a
replacement _id cannot prove which delete won and therefore remains
ambiguous_write. code === 'unique_conflict' identifies a secondary
unique-index conflict without leaking a raw MongoDB error.
🎯 Type-Safe Query Filters
SmartData provides a rich, type-safe filtering system supporting all MongoDB operators with full IntelliSense:
// Comparison operators
const adults = await User.getInstances({
age: { $gte: 18, $lt: 65 },
});
// Array operators
const experts = await User.getInstances({
tags: { $all: ['typescript', 'mongodb'] },
skills: { $size: 5 },
});
// Logical operators
const complex = await Order.getInstances({
$and: [
{ status: 'active' },
{ $or: [{ priority: 'high' }, { value: { $gte: 1000 } }] },
],
});
// Deep nested object queries
const users = await User.getInstances({
profile: {
settings: {
notifications: { email: true },
},
},
});
// Dot notation
const sameUsers = await User.getInstances({
'profile.settings.notifications.email': true,
});
// Regex patterns
const gmailUsers = await User.getInstances({
email: { $regex: '@gmail\\.com$', $options: 'i' },
});Security: The $where operator is automatically blocked to prevent NoSQL injection. Unknown operators trigger warnings.
🔎 Lucene-Powered Search
Mark fields with @searchable() to enable a built-in search engine with automatic compound text indexing:
@Collection(() => db)
class Product extends SmartDataDbDoc<Product, Product> {
@unI() public id!: string;
@svDb() @searchable() public name!: string;
@svDb() @searchable() public description!: string;
@svDb() @searchable() public category!: string;
@svDb() public price!: number;
}
// Simple text search across all @searchable fields
const results = await Product.search('laptop');
// Field-specific search
const electronics = await Product.search('category:Electronics');
// Wildcard
const matches = await Product.search('Mac*');
// Boolean operators (AND, OR, NOT)
const query = await Product.search('laptop AND NOT gaming');
// Phrase search
const exact = await Product.search('"MacBook Pro"');
// Range queries
const midRange = await Product.search('price:[100 TO 500]');
// Combined with MongoDB filters and post-fetch validation
const affordable = await Product.search('laptop', {
filter: { price: { $lte: 1500 } },
validate: async (p) => p.price > 0,
});📡 Real-Time Change Streams
Watch for database changes with RxJS subjects and EventEmitter support:
const watcher = await User.watch(
{ status: 'active' },
{
fullDocument: 'updateLookup',
bufferTimeMs: 100, // optional: buffer changes via RxJS
},
);
// RxJS subscription
watcher.changeSubject.subscribe((user) => {
console.log('User changed:', user);
});
// Or EventEmitter style
watcher.on('change', (user) => {
console.log('User changed:', user);
});
// Clean up
await watcher.close();🔄 Cursor Streaming
Process large datasets without memory pressure:
const cursorOptions = {
projection: { id: 1, username: 1, createdAt: 1 },
sort: { createdAt: -1, id: -1 },
batchSize: 100,
limit: 1000,
maxTimeMS: 10_000,
} as const;
// Iterate one-by-one
const callbackCursor = await User.getCursor(
{ status: 'active' },
cursorOptions,
);
await callbackCursor.forEach(async (user) => {
await processUser(user);
});
// Or use a separate cursor to collect into an array
const arrayCursor = await User.getCursor({ status: 'active' }, cursorOptions);
const users = await arrayCursor.toArray();toArray() always closes its cursor. forEach() closes by default, including
when hydration or the callback throws. next() returns null and closes by
default at end-of-stream, and also closes before rethrowing a hydration error.
A caller that stops a next() loop early must call close() in a finally
block.
Structured batchSize and cursor/count limit values must be positive safe
integers no greater than 10_000; maxTimeMS must be a positive safe integer
no greater than 120_000. Invalid bounds, projections, or sort paths throw
SmartdataPersistenceError with code === 'invalid_argument'. Projection and
sort paths must name top-level declared persisted fields. Add a unique final sort field,
such as id above, when equal preceding values require deterministic
pagination. Cursor, count, existence, and single-document reads accept a
caller-owned session where exposed by their options.
The raw cursor modifier option remains available for compatible callers, runs
after the structured options, and is deprecated. New code should use the
structured projection, sort, batch-size, limit, timeout, and session options.
SmartData closes the original cursor if a legacy modifier throws or returns a
different cursor.
🔐 Transactions
Use a SmartData-owned session when every operation in the transaction goes through finite SmartData model APIs. Initialize every participating model before opening the transaction:
await User.ensureInitialized();
const session = db.createSession();
try {
await session.withTransaction(async (transaction) => {
const sender = await User.getInstance(
{ id: 'user-1' },
{ session: transaction },
);
sender.balance -= 100;
await User.atomicUpdate(
{ id: sender.id, revision: sender.revision },
{ $set: { balance: sender.balance }, $inc: { revision: 1 } },
{ session: transaction },
);
const receiver = await User.getInstance(
{ id: 'user-2' },
{ session: transaction },
);
receiver.balance += 100;
await User.atomicUpdate(
{ id: receiver.id, revision: receiver.revision },
{ $set: { balance: receiver.balance }, $inc: { revision: 1 } },
{ session: transaction },
);
});
} finally {
await session.close();
}The driver retains control of TransientTransactionError callback retries and
UnknownTransactionCommitResult commit retries. After those retries finish,
an ordinary-model duplicate conflict rejects with SmartdataPersistenceError
code unique_conflict and keeps the MongoDB error as cause. Callback-thrown
SmartData and application errors preserve their identity.
Owned sessions support ordinary static inserts including insertMany(),
single/multiple reads, counts, getGroupedTotals(),
searches, atomic updates including find-one-and-update and
atomicUpdateMany(), and atomic deletes including atomicDeleteMany(). They
do not support cursor or
paged APIs, change streams, or instance save()/delete() in this release.
Use startSession() only when raw MongoDB operations or manual transaction
control are required; that raw escape hatch keeps MongoDB's raw completion
errors and uses endSession() for cleanup.
The same opaque session can coordinate exact persistence:
await Quota.exact.prepare();
await Receipt.exact.prepare();
const session = db.createSession();
try {
await session.withTransaction(async (transaction) => {
const quota = await Quota.exact.findStoredOne(
{ id: 'tenant-a:2026-07-30T14:00Z' },
{ session: transaction },
);
await Quota.exact.transition({
current: quota!,
change: (entry) => {
entry.count += 1;
},
}, { session: transaction });
await Receipt.exact.insert({
id: 'request-123',
quotaId: quota!.id,
}, { session: transaction });
});
} finally {
await session.close();
}The transaction callback may run more than once. Keep irreversible side effects outside it, do not run session operations in parallel, and treat exact result objects as transaction-local until commit. SmartData validates database ownership and lifecycle, uses the same session for reconciliation reads, and preserves transient MongoDB transaction errors for driver retry handling.
💾 EasyStore — Type-Safe Key-Value Storage
Built on top of SmartData collections, EasyStore provides simple key-value persistence:
interface AppConfig {
apiKey: string;
features: { darkMode: boolean; notifications: boolean };
limits: { maxUsers: number };
}
const config = await db.createEasyStore<AppConfig>('app-config');
// Write
await config.writeKey('features', { darkMode: true, notifications: false });
// Read
const features = await config.readKey('features');
if (features) {
// TypeScript knows: features.darkMode is boolean ✅
console.log(features.darkMode);
}
// Read all
const all = await config.readAll();
// Write multiple keys
await config.writeAll({ apiKey: 'new-key', limits: { maxUsers: 500 } });
// Delete a key
await config.deleteKey('features');
// Wipe the store
await config.wipe();🌐 Distributed Coordination
Built-in leader election using MongoDB for coordination, integrating with @push.rocks/taskbuffer:
import { SmartdataDistributedCoordinator } from '@push.rocks/smartdata';
const coordinator = new SmartdataDistributedCoordinator(db);
// Start coordination — automatic heartbeat and leader election
await coordinator.start();
// Fire distributed task requests
const result = await coordinator.fireDistributedTaskRequest({
submitterId: 'instance-1',
requestResponseId: 'unique-id',
taskName: 'process-payments',
taskVersion: '1.0.0',
taskExecutionTime: Date.now(),
taskExecutionTimeout: 30000,
taskExecutionParallel: 1,
status: 'requesting',
});
// Graceful shutdown with leadership handoff
await coordinator.stop();🎨 Custom Serialization
Transform data on its way in and out of MongoDB:
@Collection(() => db)
class Doc extends SmartDataDbDoc<Doc, Doc> {
@svDb({
serialize: (set) => Array.from(set),
deserialize: (arr) => new Set(arr),
})
public tags!: Set<string>;
@svDb({
serialize: (date) => date?.toISOString(),
deserialize: (str) => (str ? new Date(str) : null),
})
public scheduledAt!: Date | null;
}🫙 undefined Means Absent, null Means Null
SmartData distinguishes the two, so an optional field genuinely has three states:
@Collection(() => db)
class Doc extends SmartDataDbDoc<Doc, Doc> {
@unI() public id!: string;
@svDb() public note?: string; // may be absent
@svDb() public archivedAt!: Date | null; // may be null
}
const doc = new Doc();
doc.id = 'a';
doc.archivedAt = null;
await doc.save();
// stored: { id: 'a', archivedAt: null } -- `note` is not stored at all- A saveable property that is
undefinedis omitted from the stored document. It is never written asnull. - A saveable property that is explicitly
nullis stored asnull. - Nested
undefinedobject properties are dropped too. Array positions are preserved:[1, undefined, 3]persists as[1, null, 3], matchingJSON.stringify.
An omitted property is not part of the $set document, so on update it leaves
whatever is already stored alone. To actively clear a stored field, issue an
explicit $unset:
const clearResult = await Doc.atomicUpdate(
{ id: 'a' },
{ $unset: { note: true } },
);
if (clearResult.matchedCount !== 1) {
throw new Error('The document no longer exists.');
}SmartData deliberately does not unset automatically: overriding
createSavableObject() to drop a key is an established way of saying "this field
is managed elsewhere" — distributed lease ids, generation counters and similar
fields updated through atomic operators would be destroyed by an implicit
$unset on every save.
Query filters are unaffected — SmartData deliberately does not enable the
MongoDB driver's ignoreUndefined option, because that would also strip
undefined values out of filters and silently widen which documents a query
matches. A filter of { field: null } continues to match both stored null and
absent fields, which is standard MongoDB behaviour.
Migrating from < 8.0.0: earlier versions stored
undefinedasnull. Existingnulls are left untouched; only new writes change. Code that testsfield === nullto detect an absent value should test for both, or preferably usefield == null, which coversnullandundefinedalike.
🧹 Cached Documents With TTL
Use SmartdataCachedDocument for cache-like documents that need creation,
last-access, and absolute-expiration timestamps:
import {
Collection,
SmartdataCachedDocument,
smartdataTtlValues,
svDb,
unI,
} from '@push.rocks/smartdata';
@Collection(() => db)
class CachedLookup extends SmartdataCachedDocument<CachedLookup> {
@unI()
public cacheKey!: string;
@svDb()
public payload!: string;
}
const lookup = new CachedLookup();
lookup.cacheKey = 'example';
lookup.payload = 'cached value';
lookup.setTTL(smartdataTtlValues.HOURS_24);
await lookup.save();expiresAt is indexed with MongoDB TTL semantics using
expireAfterSeconds: 0, so the document expires after the absolute date stored
in expiresAt. MongoDB TTL cleanup is asynchronous and may lag behind the exact
expiration time.
🎣 Lifecycle Hooks
Add custom logic before and after save/delete:
@Collection(() => db)
class Order extends SmartDataDbDoc<Order, Order> {
@unI() public id!: string;
@svDb() public items!: Array<{ product: string; quantity: number; price: number }>;
@svDb() public totalAmount!: number;
async beforeSave() {
this.totalAmount = this.items.reduce((s, i) => s + i.price * i.quantity, 0);
}
async afterSave() {
await notificationService.orderUpdated(this.id);
}
async beforeDelete() {
if (this.totalAmount > 0) throw new Error('Cannot delete non-zero orders');
}
async afterDelete() {
await cache.delete(`order:${this.id}`);
}
}🏗️ Indexing
@Collection(() => db)
class HighPerformanceDoc extends SmartDataDbDoc<HighPerformanceDoc, HighPerformanceDoc> {
@unI()
public id!: string; // Unique index
@index()
public userId!: string; // Regular index
@index({ sparse: true })
public deletedAt?: Date; // Sparse index — only indexes docs where field exists
@index({ expireAfterSeconds: 86400 })
public expiresAt!: Date; // TTL index — auto-expires 24h after this Date
}🔧 Connection Options
const db = new SmartdataDb({
mongoDbUrl: 'mongodb://localhost:27017',
mongoDbName: 'myapp',
mongoDbUser: 'admin',
mongoDbPass: 's3cret',
// Connection pool tuning (all optional)
maxPoolSize: 100, // Max connections (default: 100)
maxIdleTimeMS: 300000, // Close idle connections after 5min (default)
serverSelectionTimeoutMS: 30000, // Timeout for server selection
socketTimeoutMS: 30000, // Socket timeout to prevent hung operations
});🩺 Bounded Readiness Checks
checkReadiness() is the public, read-only health probe for a SmartdataDb.
It reports ready only after a MongoDB { ping: 1 } round trip; consumers do not
need to access the raw driver.
const abortController = new AbortController();
const readiness = await db.checkReadiness({
timeoutMs: 1_500,
signal: abortController.signal,
});
if (!readiness.ready) {
console.warn(
`Database unavailable: ${readiness.reason} after ${readiness.durationMs}ms`,
);
} else {
console.log(`Database ready after ${readiness.durationMs}ms`);
}The discriminated result is either
{ ready: true, status: 'ready', durationMs } or
{ ready: false, status: 'unavailable', reason, durationMs }. Failure reasons
are not_initialized, connecting, connection_failed, closing, closed,
close_failed, aborted, timeout, and unavailable. Lifecycle state takes
precedence over an already-aborted signal while the database is not connected,
and a probe already in flight becomes unavailable if the connection lifecycle
or underlying driver references change.
timeoutMs defaults to 2_000 and must be a positive safe integer no greater
than 30_000. Invalid values reject with SmartdataPersistenceError code
invalid_argument. Both this deadline and the optional AbortSignal are
passed to the MongoDB command so they cancel the underlying work rather than
only racing its promise. Operational failures are returned as the typed
unavailable result without exposing a raw MongoDB response, error, or cause.
Completed init() → close() → init() sequences are supported. Starting
init() or close() while another lifecycle operation is still active rejects
with SmartdataPersistenceError code unsupported_operation.
🛠️ Bounded MongoDB Administration
createMongoAdministration() exposes the small set of privileged MongoDB
operations that infrastructure owners need without exposing the native client
or database. The facade is bound to its owning SmartdataDb, validates the
currently active connection for every operation, and fails closed if that
connection changes while an operation is in flight.
const expectedTopology = {
expectedSetName: 'onebox-rs',
expectedMemberHost: 'onebox-mongodb:27017',
timeoutMs: 5_000,
};
const bootstrapDb = new SmartdataDb({
mongoDbUrl: 'mongodb://<USERNAME>:<PASSWORD>@onebox-mongodb:27017/admin?authSource=admin&directConnection=true',
mongoDbName: 'admin',
mongoDbUser: adminUsername,
mongoDbPass: adminPassword,
});
await bootstrapDb.init();
try {
const bootstrapAdministration = bootstrapDb.createMongoAdministration();
await bootstrapAdministration.initializeSingleMemberReplicaSet(expectedTopology);
} finally {
await bootstrapDb.close();
}After initialization, replace the direct bootstrap connection with a new
SmartdataDb that names the replica set before proving transactions or
managing database users:
const replicaSetDb = new SmartdataDb({
mongoDbUrl: 'mongodb://<USERNAME>:<PASSWORD>@onebox-mongodb:27017/myapp?authSource=admin&replicaSet=onebox-rs',
mongoDbName: 'myapp',
mongoDbUser: adminUsername,
mongoDbPass: adminPassword,
});
await replicaSetDb.init();
const replicaSetAdministration = replicaSetDb.createMongoAdministration();
const readiness = await replicaSetAdministration.checkTransactionReadiness({
timeoutMs: 5_000,
});
if (!readiness.ready) {
throw new Error(`MongoDB transactions are unavailable: ${readiness.reason}`);
}
await replicaSetAdministration.ensureReadWriteDatabaseUser({
expectedDatabaseName: 'myapp',
username: 'myapp-runtime',
password: runtimeDatabasePassword,
timeoutMs: 5_000,
});
const removal = await replicaSetAdministration.removeDatabaseUser({
expectedDatabaseName: 'myapp',
username: 'myapp-runtime',
timeoutMs: 5_000,
});
console.log(removal.removed);
await replicaSetAdministration.dropDatabase({
expectedDatabaseName: 'myapp',
confirmationDatabaseName: 'myapp',
timeoutMs: 5_000,
});Replica-set inspection returns exact_primary or exact_non_primary only for
the expected set name and one-member host; any other configured topology is a
sanitized divergent result. Initialization mutates only an uninitialized
replica set, returns already_configured for an already exact topology, and
never reconfigures an existing topology. Transaction readiness creates a
reserved capability collection, writes two uniquely identified probe documents
in one transaction, aborts it, and reports ready only after proving that neither
probe document remains.
Database-user operations require the expected database name to match the
connected SmartData database exactly. ensureReadWriteDatabaseUser() creates
or updates the named user and replaces its roles with the single database-scoped
readWrite role. removeDatabaseUser() returns { removed: false } when the
user is already absent. dropDatabase() requires expectedDatabaseName to
match both the connected driver database and current SmartData descriptor, a
separately supplied exact name confirmation, and a non-system target; it rejects
the admin, config, and local databases. Every operation has a bounded deadline and optional
AbortSignal, and operational failures do not expose raw MongoDB responses or
errors.
These methods require a MongoDB credential with the corresponding administrative privileges. They are intended for infrastructure controllers and versioned migrations, not ordinary application persistence.
For isolated, disposable databases such as per-test namespaces, close() can
remove the connected database before closing its client. The caller must retain
the intended database name independently and supply it as an exact destructive
fence:
const expectedDatabaseName = testDescriptor.mongoDbName;
const db = new SmartdataDb(testDescriptor);
await db.init();
// Run the isolated test workload.
await db.close({
dropDatabase: {
expectedDatabaseName,
},
});The expected name, the connected MongoDB database, and the current SmartData
descriptor must all match exactly. Malformed options, lifecycle overlap, stale
bindings, or name mismatches reject before any session, database, connection,
status, or readiness state is changed. This option is intentionally destructive
and must not be used for shared or production databases. Ordinary close()
continues to preserve data.
📚 Decorators Reference
| Decorator | Target | Description |
|-----------|--------|-------------|
| @Collection(dbGetter, options?) | Class | Binds a document class to MongoDB with an optional explicit persisted collectionName |
| @compoundIndex({ name, key, options? }) | Class | Declares an inherited named compound index with ordered 1 / -1 directions |
| @exactPersistence(options) | Class | Enables exact persistence using one synchronous document assertion and an inferred or explicit reconciliation identity |
| @managed(managerGetterOrOptions?, options?) | Class | Like @Collection but controlled by a manager instance and optionally bound to an explicit persisted collectionName |
| @unI(options?) | Field | Marks as unique index + saveable; defaults to string identities and supports explicit valueType: 'positiveSafeInteger' for ordinary decorator models |
| @svDb(options?) | Field | Marks a field as saveable, with optional serialize, deserialize, and exact nested atomicPaths |
| @index(options?) | Field | Creates a regular MongoDB index |
| @searchable() | Field | Enables Lucene-style text search on this field |
| @globalSvDb() | Field | Marks field as globally saveable across all doc types |
📚 API Reference
Core Classes
| Class | Description |
|-------|-------------|
| SmartdataDb | Database connection, bounded readiness checks, session management, and EasyStore factory |
| SmartdataMongoAdministration | Lifecycle-bound replica-set inspection/initialization, transaction capability proof, database-user reconciliation, and exactly confirmed database removal |
| SmartdataSession | Opaque SmartData-owned ordinary/exact transaction session with withTransaction() and idempotent close() |
| SmartDataDbDoc<T, TImpl> | Base class for all document models |
| SmartdataCollection<T> | Underlying collection manager (usually accessed indirectly) |
| SmartdataExactCollection<T, TPersisted> | Validated exact insert, query, reconciliation, CAS transition, and revision-fenced delete API exposed as Model.exact |
| SmartdataExactPersistenceError | Exact-persistence contract error with typed codes including ambiguous_write and unique_conflict |
| SmartdataPersistenceError | Ordinary-model persistence error with stable codes including unique_conflict |
| SmartdataDbCursor<T> | Cursor for streaming large result sets |
| SmartdataDbWatcher<T> | Change stream watcher with RxJS + EventEmitter |
| SmartdataDistributedCoordinator | Leader election and distributed task coordination |
| SmartdataCachedDocument<T> | Base class for cache documents with creation, access, and expiration timestamps |
| EasyStore<T> | Type-safe key-value store backed by a collection |
Utilities
| Utility | Description |
|---------|-------------|
| defineCollectionModel(Model, dbOrGetter, config) | Bind a programmatically declared ordinary model with explicit fields and stable named indexes |
| ICollectionModelConfig<T> | Programmatic collection name, persisted/searchable fields, identity fields with matching explicit ascending unique indexes, and index contract |
| smartdataTtlValues | Common TTL durations in milliseconds for cache documents |
| isMongoObjectId(value) | Runtime guard for exact stored-document _id validation |
| TExactPersistedDocument<T> | Application-owned BSON document shape accepted by exact inserts |
| TStoredDocument<T> | Exact stored shape including SmartData-owned _id and optional revision metadata |
| TExact<Model> | Exact collection type inferred from a SmartData model's persisted document type |
| TPersistedOf<Model> | Persisted document type extracted from a SmartData model |
| TExactDocumentAssertion<T> | Synchronous assertion contract accepted by @exactPersistence() |
| TExactIdentityKey<T> | Required non-nullable string keys eligible for reconcileBy |
| TExactDocumentPath<T> | Bounded, type-checked dot paths through nested objects and array elements |
| TExactDocumentPathValue<T, TPath> | Value type resolved from an exact-document dot path |
| TExactDistinctValue<T> | Element type returned when distinct() targets an array field |
| TExactDeleteResult | Exact CAS-delete result: { status: 'deleted', deletedCount: 1 } or { status: 'concurrent_change', deletedCount: 0 } |
| IExactSessionOptions | Optional SmartData-owned session accepted by exact operations |
| TSmartdataOrdinarySession | Compatibility type accepted by finite ordinary-model APIs: raw MongoDB ClientSession or owned SmartdataSession |
| TExactPersistenceErrorCode | Typed exact-persistence error codes for explicit failure handling |
| TSmartdataPersistenceErrorCode | Typed ordinary-persistence codes: invalid_argument, invalid_configuration, unique_conflict, and unsupported_operation |
| TSmartdataAtomicFilter<T> | Strict ordinary-model selector for declared top-level and explicitly allowed nested paths |
| ISmartdataAtomicUpdate<T> | Typed $set, $unset, numeric $inc, $setOnInsert, and single-element array $push/$addToSet/$pull operations |
| ISmartdataAtomicUpdateOptions | Optional upsert and caller-owned session controls |
| ISmartdataAtomicUpdateResult | Authoritative acknowledged, matched, modified, and upserted outcome |
| TSmartdataAtomicReturnDocument | Required before or after return mode for atomic find-and-update operations |
| ISmartdataAtomicFindOneAndUpdateOptions<TReturnDocument, TModel> | Required return mode plus optional upsert, declared-field sort, and caller-owned session controls |
| TSmartdataAtomicFindOneAndUpdateResult<T, TReturnDocument> | Sanitized matched, upserted, or not-matched outcome with a hydrated non-null model document |
| ISmartdataAtomicDeleteOptions | Optional caller-owned session control for globally constrained and plural deletes |
| ISmartdataAtomicDeleteResult | Authoritative acknowledged and deleted outcome |
| ISmartdataAtomicUpdateManyOptions | Optional caller-owned session control for plural updates |
| ISmartdataAtomicUpdateManyResult | Authoritative acknowledged, matched, and modified plural outcome |
| ISmartdataGroupedTotalsOptions<T> | Declared groupBy/sum fields, ordinary read filter, required group bound, timeout, and optional session |
| ISmartdataGroupedTotalsRow<T> | One group's key values, count, and numeric sums |
| ISmartdataCursorOptions<T> | Structured projection, sort, batch, limit, timeout, session, and deprecated modifier controls |
| ISmartdataCountOptions | Bounded count limit, timeout, and optional session |
| ISmartdataFindOneOptions<T> | Structured projection, timeout, and optional session for one-document reads |
| ISmartdataReadinessOptions | Optional readiness timeoutMs deadline and AbortSignal |
| TSmartdataReadinessResult | Discriminated ready/unavailable result returned by SmartdataDb.checkReadiness() |
| TSmartdataReadinessFailureReason | Typed readiness failure reasons for lifecycle, cancellation, timeout, and availability failures |
| ISmartdataMongoOperationOptions | Optional bounded deadline and AbortSignal shared by MongoDB administration operations |
| ISmartdataMongoReplicaSetExpectation | Exact expected single-member replica-set topology with bounded operation controls |
| ISmartdataMongoReplicaSetMember | Sanitized observed member host, health, and state |
| TSmartdataMongoReplicaSetState | Exact-primary, exact-non-primary, divergent, disabled, uninitialized, or unavailable topology state |
| ISmartdataMongoReplicaSetInspection | Sanitized replica-set topology inspection result |
| ISmartdataMongoReplicaSetInitializationResult | Initiated or already-configured result with the latest sanitized topology |
| TSmartdataMongoTransactionReadinessFailureReason | Typed cancellation, timeout, topology, availability, or cleanup failure reason |
| TSmartdataMongoTransactionReadinessResult | Sanitized result from the two-write transaction-abort capability proof |
| ISmartdataMongoDatabaseUserOptions | Fenced read/write database-user reconciliation options |
| ISmartdataMongoDatabaseUserRemovalOptions | Fenced database-user removal options |
| ISmartdataMongoDatabaseDropOptions | Exact connected-database and repeated-name confirmation fence for non-system database removal |
Key Static Methods on SmartDataDbDoc
| Method | Description |
|--------|-------------|
| getInstances(filter, opts?) | Find multiple documents |
| getInstance(filter, opts?) | Find one document or null, with optional projection, timeout, and session |
| exists(filter, opts?) | Check existence using an _id-only read |
| getCursor(filter, opts?) | Get a bounded streaming cursor with projection, compound sort, batch size, limit, timeout, and session |
| getCount(filter?, opts?) | Count matching documents with optional limit, timeout, and session |
| getGroupedTotals(options) | Bounded server-side grouped counts and declared-numeric-field sums without raw aggregation access |
| init() / ensureInitialized() | Retry-safe model collection and index initialization |
| getIndexInfo() | Return the model's sanitized installed-index inventory |
| insert(document, opts?) | Insert a newly constructed ordinary model with typed unique-conflict errors |
| insertMany(documents, opts?) | Insert a validated batch with ordered or unordered semantics and precise inserted-document marking |
| atomicUpdate(filter, update, opts?) | Conditionally apply $set, $unset, $inc, $setOnInsert, and single-element $push/$addToSet/$pull with optional upsert/session |
| atomicFindOneAndUpdate(filter, update, opts) | Conditionally apply an update with optional declared-field sort and return a sanitized atomic before or after image without a separate reload |
| atomicDelete(filter, opts?) | Delete one ordinary document behind a filter that fully pins an identity field or a declared unqualified unique index |
| atomicDeleteMany(filter, opts?) | Delete every match behind strict declared-field validation with an explicit equality anchor |
| atomicUpdateMany(filter, update, opts?) | Update every match without upsert while maintaining atomic write timestamps |
| watch(filter, opts?) | Watch for real-time changes |
| search(query, opts?) | Lucene-style full-text search |
| forEach(filter, fn) | Iterate all matches with a callback |
| getNewId(length?) | Generate a class-prefixed unique ID |
| createSearchFilter(luceneQuery) | Convert Lucene query to MongoDB filter |
| getSearchableFields() | List all @searchable() fields |
Key Instance Methods on SmartDataDbDoc
| Method | Description |
|--------|-------------|
| save(opts?) | Insert or update the document |
| delete(opts?) | Delete the document |
| updateFromDb() | Refresh fields from the database |
| saveDeep(savedMap?) | Recursively save referenced documents |
| createSavableObject() | Serialize to a plain object for persistence |
| createIdentifiableObject() | Extract unique index fields for filtering |
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at [email protected].
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
