cry-db
v2.4.22
Published
database access with mongo
Downloads
708
Readme
cry-db
A MongoDB wrapper library providing a high-level API for database operations with built-in support for revisions, soft-delete, archiving, blocking, auditing, and real-time publish events.
Architecture
Base Connection management, ObjectId utilities, timestamps
└─ Db Database selection, collection/index management
└─ Mongo Full CRUD, soft-delete, archive, block, audit, publish events
Repo Convenience wrapper around Mongo bound to a single collectionMongo— operates on explicit(collection, query, ...)argumentsRepo<T>— wraps aMongoinstance with a fixed collection, so every call omits the collection parameter
Quick Start
import { Repo, Mongo } from 'cry-db';
// Repo — single-collection convenience class
const users = new Repo('users', 'mydb');
// Mongo — multi-collection class
const mongo = new Mongo('mydb');Connection uses MONGO_URL env var (default mongodb://127.0.0.1:27017) and MONGO_DB env var for the database name.
Record Lifecycle
Documents in cry-db have three visibility states controlled by metadata flags. Each state is opt-in and independent — a record can be in any combination of states simultaneously.
| State | Flag | Opt-in | Filtered from queries | Reversible |
|-----------|------------|--------------------------|------------------------------|------------|
| Active | (none) | — | No | — |
| Deleted | _deleted | useSoftDelete(true) | Yes, unless returnDeleted | No (use hardDelete to purge) |
| Archived | _archived| useArchive(true) | Yes, unless returnArchived | Yes (unarchiveOne) |
| Blocked | _blocked | — | No (always returned) | Yes (unblockOne) |
Query filtering rules:
- When soft-delete is enabled, all query methods (
find,findOne,findById,findByIds,count,findNewer,findNewerMany,findByIdsInManyCollections) automatically add{ _deleted: { $exists: false } }to the query. Pass{ returnDeleted: true }inQueryOptsto override. - When archive is enabled, the same query methods automatically add
{ _archived: { $exists: false } }. Pass{ returnArchived: true }to override. - Both filters apply independently. A record that is both deleted and archived is hidden by both filters — you need both
returnDeletedandreturnArchivedto see it. findAllbypasses both filters entirely.
CRUD Operations
Insert
let user = await users.insert({ name: 'Alice', age: 30 });
// user._id, user._rev, user._ts are set automatically when revisions are enabled
let many = await users.insertMany([{ name: 'Bob' }, { name: 'Carol' }]);Query
await users.find({ age: { $gte: 25 } });
await users.find({ age: { $gte: 25 } }, { sort: { name: 1 }, limit: 10, skip: 0 });
await users.findOne({ name: 'Alice' });
await users.findById(id);
await users.findByIds([id1, id2]);
await users.findAll(query); // bypasses soft-delete/archive filters
await users.count({ age: { $gte: 25 } });Update
await users.updateOne({ name: 'Alice' }, { $set: { age: 31 } });
await users.update({ active: true }, { $inc: { loginCount: 1 } }); // updateMany
await users.save({ $set: { age: 31 } }, userId);
await users.upsert({ email: '[email protected]' }, { $set: { name: 'Alice' } });Delete
await users.deleteOne({ name: 'Alice' }); // soft-delete when enabled, else hard delete
await users.delete({ inactive: true }); // bulk soft-delete / hard delete
await users.hardDeleteOne(id); // always physically removes
await users.hardDelete({}); // physically remove all matchingBatch Sync
await users.upsertBatch({
upsert: [{ _id: id1, name: 'Alice' }, { _id: id2, name: 'Bob' }],
delete: [id3],
});Features
Revisions
When enabled, every write increments _rev and updates the _ts (Timestamp) field.
users.useRevisions(true);Soft Delete
When enabled, deleteOne / delete set _deleted: Date instead of physically removing the document. Query operations automatically exclude deleted records. Deleted records cannot be "undeleted" — use hardDelete to purge them permanently.
users.useSoftDelete(true);
await users.deleteOne({ name: 'Alice' }); // sets _deleted: Date, increments _rev
await users.delete({ inactive: true }); // bulk soft-delete
await users.find({}); // excludes deleted
await users.find({}, { returnDeleted: true }); // includes deleted
await users.hardDelete({ inactive: true }); // physically removes (bypasses soft-delete)Archive
When enabled, query operations automatically exclude records with _archived set. Unlike soft-delete, archiving is fully reversible.
users.useArchive(true);
// Archive by query
await users.archiveOne({ name: 'Alice' }); // sets _archived: Date, increments _rev
await users.unarchiveOne({ name: 'Alice' }); // removes _archived, increments _rev
// Archive by id
await users.archiveOneById(id); // archive single record by _id
await users.unarchiveOneById(id); // unarchive single record by _id
// Archive multiple by ids
await users.archiveManyByIds([id1, id2, id3]); // returns array of archived docs (skips already archived)
await users.unarchiveManyByIds([id1, id2, id3]); // returns array of unarchived docs (skips non-archived)
// Query behavior
await users.find({}); // excludes archived
await users.find({}, { returnArchived: true }); // includes archivedArchive and soft-delete are independent — a record can be both archived and deleted. The returnArchived and returnDeleted options control each filter separately.
Block
Block/unblock sets or removes the _blocked field. Blocked records are not filtered from queries (unlike deleted/archived).
await users.blockOne({ name: 'Alice' });
await users.unblockOne({ name: 'Alice' });Sequences
Auto-incrementing field values managed atomically via a dedicated _sequences collection. Use the special string directives 'SEQ_NEXT' and 'SEQ_LAST' as field values during insert.
| Directive | Behavior |
|-----------|----------|
| 'SEQ_NEXT' | Increment the sequence counter and use the new value |
| 'SEQ_LAST' | Use the current sequence value without incrementing |
// Single insert — orderNo gets the next sequence value
await users.insert({ name: 'Alice', orderNo: 'SEQ_NEXT' });
// => { name: 'Alice', orderNo: 1, _id: ... }
await users.insert({ name: 'Bob', orderNo: 'SEQ_NEXT' });
// => { name: 'Bob', orderNo: 2, _id: ... }
// SEQ_LAST returns the current value (no increment)
await users.insert({ name: 'Carol', orderNo: 'SEQ_LAST' });
// => { name: 'Carol', orderNo: 2, _id: ... }Sequences are per-collection per-field. On first use, the sequence auto-seeds from the maximum existing value in the collection. If the collection is emptied, the sequence resets to 0.
Batch inserts are optimized — a range of sequence numbers is reserved atomically in a single operation, then distributed across the batch:
await users.insertMany([
{ name: 'A', orderNo: 'SEQ_NEXT' }, // orderNo: 3
{ name: 'B', orderNo: 'SEQ_LAST' }, // orderNo: 3 (current value after A)
{ name: 'C', orderNo: 'SEQ_NEXT' }, // orderNo: 4
]);Multiple fields can use independent sequences in the same document:
await users.insert({ invoiceNo: 'SEQ_NEXT', lineNo: 'SEQ_NEXT' });To reset a collection's sync sequence:
await users.resetCollectionSync();Auditing
Records changes to a separate audit log collection.
users.useAuditing(true);
users.auditToCollection('dblog');
users.auditCollection(); // enable auditing for this repo's collection
await users.dbLogGet(entityId); // retrieve audit log for an entity
await users.dbLogPurge(entityId); // purge audit logPublish Events
Real-time events emitted on insert/update/delete for data synchronization.
users.emitPublishEvents(true); // full document payloads
users.emitPublishRevEvents(true); // lightweight revision-only payloads
users.on('publish', (channel, data) => { /* ... */ });
users.on('publishRev', (channel, data) => { /* ... */ });Transactions
await users.startTransaction();
// ... operations ...
await users.commitTransaction();
// or
await users.abortTransaction();
// or callback style:
await users.withTransaction(async (client, session) => {
// ... operations ...
});Entity Metadata
Every document can have these system fields (managed automatically when the respective feature is enabled):
| Field | Type | Description |
|------------|-----------|-----------------------------------------------|
| _id | ObjectId | Document identifier |
| _rev | number | Revision counter (incremented on each write) |
| _ts | Timestamp | Server timestamp (set on each write) |
| _deleted | Date | Soft-delete timestamp |
| _archived| Date | Archive timestamp |
| _blocked | Date | Block timestamp |
QueryOpts
Options passed to query methods:
| Option | Type | Description |
|-------------------|----------------------|----------------------------------------------|
| project | Projection | Fields to include/exclude |
| sort | Record<string, 1|-1> | Sort specification |
| limit | number | Max documents to return |
| skip | number | Documents to skip |
| collation | CollationOptions | MongoDB collation options |
| readPreference | ReadPreference | Read preference |
| returnDeleted | boolean | Include soft-deleted records |
| returnArchived | boolean | Include archived records |
Additional Methods
users.distinct<string>('status'); // distinct field values (sorted)
users.findNewer(timestamp, query, opts); // find records newer than timestamp
mongo.findNewerMany([{ collection, timestamp }]); // batch findNewer across collections
mongo.findByIdsInManyCollections([{ collection, ids }]); // batch findByIds across collections
users.isUnique('email', '[email protected]', excludeId); // uniqueness check
users.aggregate(pipeline); // aggregation pipeline
users.createIndex('name_1', { name: 1 }); // create index
users.indexes(); // list indexesEnvironment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| MONGO_URL | mongodb://127.0.0.1:27017 | MongoDB connection URL |
| MONGO_DB | — | Default database name |
| AUDIT_COLLECTIONS | — | Comma-separated list of collections to audit |
Testing
npm testRequires a running MongoDB instance on localhost.
