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

@rivyn/db

v0.3.0

Published

Client for the Rivyn self-hosted NoSQL database

Readme

@rivyn/db

TypeScript client for the Rivyn self-hosted NoSQL database. MongoDB-like API, zero runtime dependencies.

Install

npm i @rivyn/db

Requires Node 20.19+ (or 22.12+). The package is ESM, but Node's require(esm) support means it works from CommonJS projects too:

import { RivynClient } from "@rivyn/db";   // ESM
const { RivynClient } = require("@rivyn/db"); // CommonJS

You also need a running @rivyn/db-server instance (local machine or your own VPS).

Quick start

import { RivynClient } from "@rivyn/db";

const client = new RivynClient({
  host: "127.0.0.1",
  port: 7223,
  key: "your-auth-key",
});

interface User {
  name: string;
  age: number;
  [key: string]: unknown;
}

const users = client.collection<User>("users");

await users.insert({ name: "arel", age: 21 });
await users.insertMany([{ name: "mert", age: 17 }, { name: "zeynep", age: 25 }]);

const adults = await users.find({ age: { $gt: 18 } }, { sort: { age: -1 }, limit: 10 });
const arel = await users.findOne({ name: "arel" });

await users.updateOne({ name: "arel" }, { $inc: { age: 1 }, $set: { active: true } });
await users.deleteMany({ age: { $lt: 18 } });

console.log(await users.count());
client.close();

The client connects lazily on the first request (or call client.connect() explicitly) and automatically reconnects with exponential backoff if the connection drops. Requests in flight when a connection drops are rejected rather than buffered, so a failed write always surfaces as an error instead of disappearing silently.

Schemas (Mongoose-style)

Collections are schemaless by default. For validated, structured data use Schema + client.model():

import { RivynClient, Schema, RivynValidationError } from "@rivyn/db";

const userSchema = new Schema(
  {
    name: { type: "string", required: true, minLength: 2 },
    email: { type: "string", required: true, unique: true, match: /^\S+@\S+$/ },
    age: { type: "number", min: 0, max: 150, default: 18 },
    role: { type: "string", enum: ["user", "admin"], default: "user" },
    joinedAt: { type: "date", default: null },
    xp: { type: "number", default: 0, index: true },
    tags: { type: "array", of: "string", default: [] },
    profile: { type: "object", fields: { city: { type: "string", required: true } } },
    settings: { type: "object", default: {} },   // free-form map
    nickname: "string",
  },
  { timestamps: true }
);

const User = client.model<UserType>("users", userSchema);

const arel = await User.create({ name: "arel", email: "[email protected]" });
// → defaults applied, createdAt/updatedAt set, _id returned

await User.updateOne({ email: "[email protected]" }, { $set: { role: "admin" } });
// → $set values validated, updatedAt bumped automatically

await User.create({ name: "x" });
// → throws RivynValidationError locally (never reaches the server)

Field types

string, number, boolean, date, object (with nested fields), array (with of), any. A bare string is shorthand: nickname: "string".

date accepts a Date, an ISO string or epoch milliseconds and always stores an ISO string, so values survive JSON unchanged. ISO strings sort chronologically, so $gt/$lt range filters work as expected:

await User.find({ joinedAt: { $gte: new Date("2026-01-01") } });

Read values back as strings — wrap them when you need date arithmetic: new Date(user.joinedAt).

An object with no fields is a free-form map. Any path beneath it is allowed, which is how you model what would have been a Mongoose Map:

await User.updateOne({ id }, { $set: { "settings.theme": "dark", "settings.locale.tz": "UTC" } });

Rules

required, default (value or function), nullable, enum, min/max, minLength/maxLength, match (RegExp), validate (custom function returning true or an error message), unique, index.

  • nullable: true permits an explicit null. It is inferred automatically when default is null, so { type: "string", default: null } needs no extra flag.

  • unique: true creates a unique index on the server the first time the model writes.

  • index: true creates a secondary index, so equality and range filters on that field skip the full scan. Declaring it inside an array's element definition builds a multikey index — punishments.active below indexes the flag of every element:

    new Schema({
      punishments: {
        type: "array",
        of: { type: "object", fields: { active: { type: "boolean", index: true } } },
        default: [],
      },
    });
    
    await User.find({ "punishments.active": true }); // index-backed
  • timestamps: true manages createdAt/updatedAt ISO strings for you.

Behaviour

  • Unknown fields are stripped on create; $set on a field that is not in the schema throws.
  • Updates are validated too: $set values against field rules, $inc only on number fields, $push/$addToSet elements against the array's of type (including inside $each).
  • Validation failures throw RivynValidationError with an issues array ({ path, message }), before anything is sent to the server.
  • Like Mongoose, validation runs client-side only — the server stays schemaless, so evolving your schema never conflicts with old documents. Updates validate just the paths they touch.
  • Optional server-side enforcement: to make the server reject invalid writes from any client (MongoDB's $jsonSchema equivalent), push the schema manually: await collection.setSchema(mySchema.toServerSpec()). Clear it with setSchema(null), inspect it with getSchema(). The server then validates the whole resulting document on every write, so documents predating the schema may need migrating first.

Model exposes the same query API as Collection (find, findOne, count, deleteOne, deleteMany, findOneAndUpdate, findOneAndDelete, stats), plus create/createMany instead of raw inserts. The raw collection stays available as User.collection.

create() applies schema defaults; an upsert does not — it seeds the new document from the filter and then applies the update. For "fetch or create with full defaults", read first and fall back to create().

Options

new RivynClient({
  host: "1.2.3.4",          // required
  port: 7223,               // default 7223
  key: "...",               // required — the server's auth key
  tls: false,               // true, or { rejectUnauthorized: false } for self-signed certs
  requestTimeoutMs: 10000,  // per-request timeout
  reconnect: true,          // auto-reconnect on connection loss
  reconnectDelayMs: 500,    // initial backoff delay
  maxReconnectDelayMs: 10000,
});

API

Client

| Method | Description | | --- | --- | | connect() | Connect + authenticate; returns { server, version } | | ping() | Round-trip health check | | collection<T>(name) | Get a typed collection handle | | model<T>(name, schema) | Get a schema-backed model | | collections() | List collections with document counts | | stats() | Server-wide stats (uptime, docs, memory) | | dropCollection(name) | Delete a collection and its data | | on("connect" \| "disconnect", fn) | Connection lifecycle events | | close() | Close the connection (disables reconnect) |

Collection

| Method | Returns | | --- | --- | | insert(doc) | { insertedId } | | insertMany(docs) | { insertedIds } | | find(filter?, { sort, limit, skip }?) | WithId<T>[] | | findOne(filter?) | WithId<T> \| null | | updateOne(filter, update, opts?) / updateMany | { modifiedCount, upsertedId? } | | deleteOne(filter) / deleteMany | { deletedCount } | | count(filter?) | number | | aggregate(pipeline) | Row[]$match, $group ($sum/$avg/$min/$max), $sort, $limit, $skip | | findOneAndUpdate(filter, update, opts?) | WithId<T> \| null | | findOneAndDelete(filter) | WithId<T> \| null | | stats() | CollectionStats | | createIndex(field, { unique }?) | { field, unique } | | dropIndex(field) | boolean | | indexes() | IndexDef[] |

Update options (opts): { upsert?: boolean, arrayFilters?: Record<string, unknown>[], returnNew?: boolean }.

Queries

Filters support $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $regex (with $options), $elemMatch, $or, $and.

Dotted paths reach through arrays. "punishments.id" matches if any element has that id:

await users.find({ "punishments.id": 3 });
await users.find({ "punishments.oldRoles.id": "r1" });   // nested arrays too

$elemMatch requires all conditions to hold on the same element:

await users.find({ punishments: { $elemMatch: { active: true, type: "ban" } } });

$regex takes flags through $options:

await users.find({ name: { $regex: "^ar", $options: "i" } });

Updates

$set, $unset, $inc, $push, $pull, $addToSet, or a plain object for full replacement.

$each spreads several values into $push or $addToSet; $addToSet compares by value, so objects are deduplicated properly:

await users.updateOne({ id }, { $push: { reasons: { $each: ["spam", "raid"] } } });
await users.updateOne({ id }, { $addToSet: { roles: { $each: [{ id: "r1" }, { id: "r2" }] } } });

Positional operators

$ updates the element the filter matched, $[] updates every element, and $[name] updates the elements picked by arrayFilters:

// The punishment with id 2 — the one the filter selected.
await users.updateOne(
  { id: "550", "punishments.id": 2 },
  { $set: { "punishments.$.active": false } },
);

// Every punishment.
await users.updateMany({}, { $set: { "punishments.$[].reviewed": true } });

// Rewrite one role id wherever it appears, at any depth.
await users.updateMany(
  { "punishments.oldRoles.id": oldId },
  { $set: { "punishments.$[].oldRoles.$[role].id": newId } },
  { arrayFilters: [{ "role.id": oldId }] },
);

Upsert

await users.updateOne(
  { id: "550" },
  { $set: { username: "arel" } },
  { upsert: true },
);

When nothing matches, the document is seeded from the filter's plain equality fields ({ id: "550" }, and dotted keys like "a.b": 1 become { a: { b: 1 } }), then the update is applied. Operator conditions carry no single value, so they are skipped.

Server-side errors (unique index violations, invalid operations) are thrown as RivynError.

Development

npm install
npm test        # runs against a real server spawned from ../server
npm run build

License

MIT