npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cry-db

v2.5.0

Published

database access with mongo

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 collection
  • Mongo — operates on explicit (collection, query, ...) arguments
  • Repo<T> — wraps a Mongo instance 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 } in QueryOpts to 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 returnDeleted and returnArchived to see it.
  • findAll bypasses 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' } });

Bracket-by-_id array paths

Update array elements by their _id instead of by position. The path arr[<id>] is shorthand for "the element of arr whose _id is <id>", resolved server-side against the live document — no race window where a concurrent reorder, insert, or delete shifts indices between client read and server write.

Supported in every write method: updateOne, save, update, upsert, upsertBatch, and updateCollections. Top-level shorthand (no $set wrapper) is normalized into $set automatically; key: undefined becomes $unset as usual.

Syntax

| Form | Quoted variants | |---|---| | arr[<id>] | arr['<id>'], arr["<id>"] |

Quoted and unquoted forms are equivalent. Use quotes when an _id contains characters that could conflict with dot-notation (rare).

Four operations

// 1. Update sub-field of element with _id === 'p2'
//    (translated to $set + arrayFilters [{ f0._id: 'p2' }])
await items.updateOne(
    { _id: 'r1' },
    { 'postavke[p2].kolicina': 99 },
);

// 2. Unset sub-field of element with _id === 'p2'
//    (key: undefined → $unset → mongo $unset on the matched element)
await items.updateOne(
    { _id: 'r1' },
    { 'postavke[p2].navodilo': undefined },
);

// 3. Insert / replace an element by _id (terminal bracket + array value)
//    Idempotent — re-sending the same _id replaces in place, no duplicates.
//    Each element MUST carry its own `_id`.
await items.updateOne(
    { _id: 'r1' },
    { "postavke['p0']": [{ _id: 'p0', value: 0 }] },
);

// 4. Remove an element by _id (terminal bracket + undefined)
//    Translated to $pull (or merged into the same pipeline as inserts above).
await items.updateOne(
    { _id: 'r1' },
    { "postavke['p1']": undefined },
);

Single-element insert/remove uses an array of one element on the value side (form 3). Multiple terminal-bracket inserts against the same parent field are merged into one atomic stage.

Combining operations

Sub-field updates (form 1/2) and terminal-bracket inserts/removes (form 3/4) can both appear in a single update with one exception:

| In the same update | Result | |---|---| | Sub-field updates only (1, 2) | Single doc update with arrayFilters. | | Terminal removes only (4) | $pull added to the doc; arrayFilters still allowed for any sub-field updates. | | Terminal inserts (3), with or without removes (4) | Aggregation pipeline with a unified $filter + $concatArrays stage per parent field. Inserts are idempotent (re-inserting the same _id replaces the element). | | Terminal inserts and sub-field updates on the same parent array | Single pipeline: $filter + $concatArrays produces the post-insert array, then a per-_id $switch + $mergeObjects overlay applies the sub-field updates LAST. Same-id insert + sub-field update yields an element with the insert as its base and the sub-field update overriding matching fields. |

Nested brackets (multi-level)

Bracket paths can chain to arbitrary depth — arr[A].sub[B].field, arr[A].sub[B], arr[A].sub[B] = undefined, and so on are all supported. Pure sub-field nested updates take the arrayFilters fast path (mongo's native nested positional support). When combined with a terminal-bracket insert/remove anywhere in the update, the library switches to pipeline form and composes the nested transform recursively: each bracket level becomes a $filter + $concatArrays + $map/$switch stage applied inside the parent element's overlay.

// ✓ Pure nested sub-field — arrayFilters path
await items.updateOne({ _id: 'r1' }, {
    'terapije[t1].postavke[sp2].kolicina': 99,
});

// ✓ Nested sub-field + nested terminal insert (different ids) — pipeline path
await items.updateOne({ _id: 'r1' }, {
    'terapije[t1].postavke[sp2].kolicina': 99,
    "terapije[t1].postavke[sp3]": [{ _id: 'sp3', kolicina: 5 }],
});

// ✓ Combined with top-level operations
await items.updateOne({ _id: 'r1' }, {
    'terapije[t1].postavke[sp2].kolicina': 99,
    "terapije[t1].postavke[sp3]": [{ _id: 'sp3', kolicina: 5 }],
    "terapije[t2]": undefined,                            // delete outer t2
    "terapije[t9]": [{ _id: 't9', postavke: [] }],        // insert new outer t9
});

Order of effects within one update:

  1. $filter removes elements whose _id is in any removeIds for that level.
  2. $concatArrays appends terminal-bracket-inserted elements at the end.
  3. $map + $switch applies sub-field overlays in-place on matching ids.
  4. Nested array transforms inside the overlay are wrapped in $cond: $isArray(<path>) — when the operation is removes-only on a missing nested array, the field is NOT created. (Matches mongo's native "$pull on missing field = no-op" semantic.)

Same-id overlap: when a nested "new base" operation and a nested sub-field update target the SAME element (same _id at the inner level), both apply — the new-base value is set first, sub-field overlay applies on top. A kind: 'overlap' warning is emitted so the unusual overlap is visible to the caller.

Two flavors trigger the warning:

// Flavor 1: terminal-array insert + sub-field on same id.
await items.updateOne({ _id: 'r1' }, {
    'terapije[t1].postavke[sp1].kolicina': 99,
    "terapije[t1].postavke[sp1]": [{ _id: 'sp1', q: 5 }],
});
// final sp1: { _id: 'sp1', q: 5, kolicina: 99 }
//   — terminal value's `q` kept, sub-field's `kolicina` overlaid.
//   — original sp1 fields (not in terminal value) are dropped.

// Flavor 2: whole-element replace (object value, not array) + sub-field.
await items.updateOne({ _id: 'r1' }, {
    'terapije[t1].postavke[sp1].kolicina': 99,
    "terapije[t1].postavke[sp1]": { _id: 'sp1', q: 5 },
});
// final sp1: identical to Flavor 1 above.

Both flavors emit result.results.warnings[0].error matching /same-id|overlap/i.

The top-level same-id case is the same semantic (insert/replace + sub-field on the same outer _id) but is silent — it's a documented merge pattern, not an overlap warning.

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 matching

Batch Sync

upsertBatch for single-collection batches:

await users.upsertBatch({
    upsert: [{ _id: id1, name: 'Alice' }, { _id: id2, name: 'Bob' }],
    delete: [id3],
});

updateCollections for multi-collection batches with per-record error/warning reporting:

const results = await mongo.updateCollections([
    [{
        collection: 'users',
        batch: {
            updates: [
                { _id: id1, _rev: 4, update: { name: 'Alice' } },
                { _id: id2, _rev: 1, update: { name: 'Bob' } },
            ],
            deletes: [{ _id: id3, _rev: 2 }],
        },
    }],
]);

Each updates/deletes entry carries _rev — the revision the client's change was computed against. The server uses it to detect concurrent external writes and to surface server-resolved fields (see mustRefresh below). Do not put _rev inside update (it is stripped). Omitting _rev is treated as 0, which forces a full-record refresh — the old-client fallback.

Partial-failure semantics: per-record failures inside updates or deletes are caught, surfaced in result.results.errors, and do not stop the rest of the batch. Each findOneAndUpdate / findOneAndDelete runs in its own try/catch, so a single Document not found, validation error, or write conflict only affects that record.

Caveat: a few code paths run outside the per-record try/catch and will still throw out of the call — abort the rest of the batch:

  • connection failure on the initial connect()
  • sequence pre-processing (_processSequenceFieldForMany), which runs once per (collection, batch) before the per-record loop
  • audit writes and publish emits, which run after the per-record loop

In practice: per-record validation / "not found" / write-conflict failures land in errors and the batch keeps going. Infrastructure failures (connection lost, audit collection unreachable, …) propagate as a thrown exception.

results[0].results.inserted    // DbEntity[] — records actually upserted as new
results[0].results.updated     // DbEntity[] — records actually updated
results[0].results.deleted     // DbEntity[] — records actually deleted
results[0].results.mustRefresh // (DbEntity & full record)[] — see below
results[0].results.errors      // { _id, error }[] — per-record failures (omitted if none)
results[0].results.warnings    // { _id, error }[] — non-fatal issues (e.g. dropped
                               //                     NESTED-bracket paths; omitted if none)

errors and warnings are arrays — one entry per affected record. Both fields are omitted when empty. The call itself does not throw on per-record failures; check .errors.length to detect them.

mustRefresh — records the client must re-adopt

The inserted/updated/deleted arrays carry only {_id,_ts,_rev}, so a client that relied on them alone never learned what cry-db changed beyond the diff it sent. mustRefresh closes that: it holds the full, sanitized post-write record for any entry where the client's local copy is now stale. A record lands in mustRefresh when:

  • cry-db resolved a field the client sent as a placeholder — SEQ_* (auto-increment) or __hashed__* (hashing); or
  • the stored _rev advanced beyond the client's base _rev by more than this write applied — i.e. another actor modified the record first (conflict); or
  • the request omitted _rev (old-client fallback — always refresh).

Contract:

  • Every mustRefresh._id also appears in inserted/updated/deleted, so dirty-clearing logic that keys off those arrays is unaffected — treat mustRefresh as supplementary.
  • Records are full and sanitized: __hashed__* values are never included (the client adopting the record drops any local plaintext), and removeFieldsAlways/findNewerRemoveFields are applied as on reads.
  • The client should replace its local record with the mustRefresh entry, then run its own dirty-merge — apply mustRefresh before the merge so unsynced local edits to other fields are reconciled rather than clobbered.
  • Conflict detection requires useRevisions(). Field-resolution refresh (SEQ/hash) works regardless of revisions.

Consumers (e.g. cry-synced-db-client): send _rev per update/delete and apply mustRefresh on the response. The applyObjDiff/computeObjDiff helpers (also exported from this package) can drive the local replace.

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 archived

Archive 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. Audited operations: insert, insertMany, update, updateOne, upsert, save, deleteOne, delete, blockOne, unblockOne, and updateCollections.

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 log

Publish 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) => { /* ... */ });

Update payload shape: data and rawUpdate

For update / updateMany events (and per-item updates inside batch events), the publish payload carries two complementary fields:

| Field | Contents | Mongo operators? | |---|---|---| | data | Clean (sub-)document with the de-bracketed result. Top-level scalar updates produce a flat key/value pair; bracket-by-_id paths produce the full post-update parent array (no diff, no positional placeholders, no $[…]). Nothing is synthesized — values are sliced straight from the post-update document. | Never — no $set, $unset, $pull, $[fN], no flat dotted/bracket keys. | | rawUpdate | The user's update spec exactly as it came in (top-level shorthand or $set/$unset form, bracket paths preserved). For inspection / replay / audit. | Allowed — that's the point. |

// Sub-field update via bracket-by-_id
await items.updateOne({ _id: 'r1' }, { 'postavke[p2].kolicina': 99 });

// Publish event:
{
    operation: 'update',
    db: 'mydb',
    collection: 'items',
    data: {
        _id: 'r1',
        postavke: [                       // FULL post-update array
            { _id: 'p1', kolicina: 1 },
            { _id: 'p2', kolicina: 99 },
            { _id: 'p3', kolicina: 3 },
        ],
    },
    rawUpdate: { 'postavke[p2].kolicina': 99 },
}

data only contains fields that were touched by the update (plus _id, plus _rev/_ts when revisions are enabled). Sibling fields the update did not touch never leak in. For terminal-bracket inserts/removes (arr[id]: [els] / arr[id]: undefined) the parent array is likewise emitted in full.

For updateMany the data field is { n: number, ok: boolean } and rawUpdate is not included — mongo's updateMany doesn't return the list of affected _id-s, so the spec on its own is not actionable. Subscribers refetch by timestamp via findNewer* (the matching publishRev event always sets enquireLastTs: true).

For batch events (upsertBatch / updateCollections), each item in payload.data carries its own data (clean) and rawUpdate (raw form).

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.findNewerMany([{ collection, timestamp, specId }]); // specId optional — disambiguates duplicate-collection specs
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 indexes

users.findNewerRemoveFields();                         // get current list of stripped fields
users.findNewerRemoveFields('_ts,_rev');               // set fields to strip (comma-separated string)
users.findNewerRemoveFields(['_ts', '_rev']);           // set fields to strip (array)

users.removeFieldsAlways();                            // get current list of always-stripped fields
users.removeFieldsAlways('secret,internal');            // set fields stripped from ALL find* and publish
users.removeFieldsAlways(['secret', 'internal']);       // set fields stripped from ALL find* and publish

Diff Utilities

Pure, mongo-independent helpers for computing and replaying document diffs, exported from the package root. They are the server-side counterpart of the cry-synced-db-client diff machinery (computeObjDiff / applyObjDiff).

import { computeObjDiff, applyObjDiff } from 'cry-db';

// Minimal MongoDB-style dot/bracket paths between two documents (only keys
// present in `update`). Arrays of `_id`-keyed objects diff element-wise via
// `arr[<id>].field`, with composition changes as `arr[<id>] = [el]` /
// `arr[<id>] = undefined`. `_ts`/`_rev`/`_csq` are never emitted.
const diff = computeObjDiff(existing, update);
// → { "koraki[k1].diag": "new", "koraki[k2]": [{ _id: "k2", ... }] }

// Replay a diff onto a base document the way a server-side `$set`+`$unset`
// would. Returns a fresh object (base is cloned, never mutated). Bracket
// paths whose parent array is missing are materialized; unrepresentable
// multi-bracket paths are dropped (logged unless the root field starts `_`).
const merged = applyObjDiff(base, diff, fallbackId, collection);

Environment 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 | | REMOVE_FIELDS_FOR_FINDNEWER | — | Comma-separated fields stripped from findNewer* results and publish events (also configurable at runtime via findNewerRemoveFields()) | | REMOVE_FIELDS_ALWAYS | — | Comma-separated fields stripped from all find* results and publish events (also configurable at runtime via removeFieldsAlways()) |

Testing

npm test

Requires a running MongoDB instance on localhost.