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

scriva

v1.2.1

Published

TypeScript/JavaScript client for ScrivaDB — a file-based document database with a gRPC API

Downloads

171

Readme

ScrivaDB — TypeScript / JavaScript Client

Node.js 18+ gRPC client for ScrivaDB.

npm package: scriva


Requirements

  • Node.js 18+
  • TypeScript 5+ (optional — plain JavaScript works too)
  • A running ScrivaDB server (make run from the repo root)

Install

npm install scriva

Build (from source)

cd clients/js
npm install
npm run build      # compiles TypeScript → dist/

Quick start

import { ScrivaDB } from 'scriva';

const db = new ScrivaDB('localhost', 5433, 'dev-key');

await db.createCollection('users');

const id = await db.insert('users', { name: 'Alice', age: 30, role: 'admin' });

const record = await db.findById('users', id);
console.log(record); // { id: '1', key: '', rev: '1', data: { name: 'Alice', age: 30, role: 'admin' }, ... }

const admins = await db.findAll('users', {
  filter: { field: 'role', op: 'eq', value: 'admin' },
  orderBy: [{ field: 'name' }],
});

await db.update('users', id, { name: 'Alice', age: 31, role: 'superadmin' });
await db.delete('users', id);
await db.dropCollection('users');

db.close();

CommonJS also works:

const { ScrivaDB } = require('scriva');

API reference

Constructor

// Plaintext (no TLS)
const db = new ScrivaDB(host: string, port: number, apiKey: string);

// TLS — verify server against a CA certificate PEM buffer
const db = new ScrivaDB(host, port, apiKey, tlsCaCert: Buffer);

// TLS — load CA certificate from file path
const db = ScrivaDB.fromTlsCertPath(host, port, apiKey, '/path/to/ca.crt');

x-api-key is attached as gRPC metadata on every call automatically.


Collection management

const name: string      = await db.createCollection('col');
const ok: boolean       = await db.dropCollection('col');
const names: string[]   = await db.listCollections();

// Optional per-collection default TTL (seconds) — records without their own TTL
// expire after this long. Persisted; overrides the server-wide default.
await db.createCollection('sessions', 3600);

CRUD

// Insert one record — returns the assigned ID (string)
const id: string = await db.insert('col', { field: 'value' });

// Insert multiple records — returns IDs in insertion order
const ids: string[] = await db.insertMany('col', [
  { name: 'Alice' },
  { name: 'Bob' },
]);

// Find by ID
const record: DBRecord = await db.findById('col', id);

// Find by ID with a field projection (N2) — only these fields land in `data`;
// id, key and rev are always included.
const partial: DBRecord = await db.findById('col', id, { fields: ['name'] });

// Streaming find — use `for await`
for await (const record of db.find('col', { filter, limit, offset, orderBy })) {
  console.log(record);
}

// Convenience: collect all results into an array
const results: DBRecord[] = await db.findAll('col', { filter });

// Update — returns the updated ID
const updatedId: string = await db.update('col', id, { field: 'new value' });

// Delete — returns true if record existed
const deleted: boolean = await db.delete('col', id);

FindOptions accepts:

| Option | Type | Notes | |---------------|-------------------|-------| | filter | FilterInput | See Filter syntax. | | limit | number | Max results, 0 = no limit. | | offset | number | Skip N leading rows (prefer pageToken for large offsets). | | orderBy | OrderByInput[] | Multi-field sort (N3): [{ field, desc? }, …], applied in order. | | fields | string[] | Field projection (N2): only these top-level fields are returned in data. | | pageToken | string | Keyset cursor (N3) — see Pagination. | | orderByField, descending | string, boolean | Deprecated single-field sort; honoured only when orderBy is empty. |

Pagination (keyset cursor, N3)

findPage returns one page plus an opaque cursor for the next. Pass an ordering and a limit, then feed the returned pageToken back on the next call — keep the ordering, filter and limit identical across pages. An empty pageToken means the last page was reached.

let token = '';
do {
  const page = await db.findPage('col', {
    orderBy: [{ field: 'age' }],
    limit: 100,
    pageToken: token,
  });
  for (const r of page.records) console.log(r.id);
  token = page.pageToken;
} while (token);

Each write takes an optional trailing ttlSeconds argument. When greater than 0 the record expires that many seconds from the write, overriding the collection default:

await db.insert('col', { field: 'value' }, 60);          // expires in 60s
await db.insertMany('col', [{ n: 1 }, { n: 2 }], 120);   // whole batch in 120s
await db.update('col', id, { field: 'v' }, 30);          // reset expiry to 30s
// ttlSeconds omitted (or 0): insert/insertMany apply the collection default;
// update leaves any existing deadline untouched (a plain update is sticky).

DBRecord shape:

interface DBRecord {
  id: string;                       // uint64 returned as string
  key: string;                      // caller-supplied string key ('' if none)
  rev: string;                      // per-record revision, '1' on insert, bumped per write
  data: Record<string, unknown>;
  date_added?: string;
  date_modified?: string;
}

Keyed CRUD, Upsert & compare-and-swap (N1)

Records can carry a caller-supplied string key and expose a monotonic revision for optimistic concurrency.

// Keyed insert — set a key on a plain insert. A key already held by a live
// record is rejected with a gRPC ALREADY_EXISTS error.
const id = await db.insert('col', { name: 'Alice' }, 0, 'user:alice');

// Upsert — insert under a key, or atomically replace if it already exists.
// Returns the resulting record (rev starts at '1', bumped on each replace).
const rec: DBRecord = await db.upsert('col', 'user:alice', { name: 'Alice', tier: 'pro' });

// Find by key — resolves to null when the key is absent (NOT_FOUND → null).
const found: DBRecord | null = await db.findByKey('col', 'user:alice');
const partial = await db.findByKey('col', 'user:alice', { fields: ['name'] });

// Update by key — overwrites the record, preserving the key. Returns the write
// acknowledgement with the new rev. Throws a gRPC NOT_FOUND if the key is absent.
const w: WriteResult = await db.updateByKey('col', 'user:alice', { name: 'Alice', tier: 'vip' });
console.log(w.rev);

// Delete by key — true if a record was removed, false when absent (NOT_FOUND → false).
const gone: boolean = await db.deleteByKey('col', 'user:alice');

// Compare-and-swap — apply only if the record's current rev matches. A stale
// rev (or a missing key) is a clean no-op: { swapped: false, record: null }.
const cur = await db.findByKey('col', 'user:alice');
const res: CasResult = await db.updateIfRev('col', 'user:alice', cur!.rev, { name: 'Alice', tier: 'vip' });
if (!res.swapped) console.log('lost the race — retry');

Aggregations (N4)

Compute count and numeric aggregations (sum/avg/min/max) in the engine, honouring the same filter as find, optionally grouped by a field.

// Count matching records.
const total: number = await db.count('col');
const admins: number = await db.count('col', { field: 'role', op: 'eq', value: 'admin' });

// Group-by with numeric aggregations. Returns one AggregateResult per group,
// in ascending group order.
const byRole: AggregateResult[] = await db.groupBy('col', 'role', {
  field: 'age',
  aggregations: ['sum', 'avg', 'min', 'max'],
});
for (const g of byRole) {
  console.log(g.group, g.count, g.numeric ? { sum: g.sum, avg: g.avg, min: g.min, max: g.max } : {});
}

// Full form — filter + group + aggregations.
const results = await db.aggregate('col', {
  filter: { field: 'active', op: 'eq', value: 'true' },
  groupBy: 'status',
  field: 'total',
  aggregations: ['sum'],
});

AggregateResult shape:

interface AggregateResult {
  group: unknown;    // group-by value (type-preserved); null for the ungrouped result
  count: string;     // records in the group (uint64 as string)
  numeric: boolean;  // true when the numeric aggregates below are meaningful
  sum?: number; avg?: number; min?: number; max?: number;
}

Secondary indexes

await db.ensureIndex('col', 'fieldName');
const ok: boolean       = await db.dropIndex('col', 'fieldName');
const fields: string[]  = await db.listIndexes('col');

Once an index exists, findAll / find with a single eq filter on that field uses the index automatically — no query hint needed.


Transactions

const txId: string  = await db.beginTx('col');
const ok: boolean   = await db.commitTx(txId);
const ok: boolean   = await db.rollbackTx(txId);

Watch (streaming change feed)

for await (const event of db.watch('col')) {
  console.log(event.op, event.record.id, event.record.data);
  // event.op: 'INSERTED' | 'UPDATED' | 'DELETED' | 'OVERFLOW'
  // OVERFLOW means the server dropped events because this subscriber fell behind
  // (no record is set) — resync by re-reading the collection.
}

With an optional filter — only matching events are delivered:

for await (const event of db.watch('col', { field: 'role', op: 'eq', value: 'admin' })) {
  // ...
}

Break out of the for await loop to stop watching.


Stats

const s = await db.stats('col');
// s.collection, s.record_count, s.segment_count, s.dirty_entries, s.size_bytes (all strings)

Maintenance

// Force a synchronous compaction pass — merges/deduplicates sealed segments and
// reclaims space from deleted or expired records. Resolves true on success.
const ok: boolean = await db.compact('col');

Backup

// Stream a consistent gzip snapshot of the whole database straight to a file.
// Resolves with the number of bytes written; restore with `tar xzf backup.tar.gz`.
const bytes: number = await db.snapshotToFile('backup.tar.gz');

// Or consume the raw gzip byte chunks yourself (Snapshot is server-streaming):
for await (const chunk of db.snapshot()) {
  // chunk: Buffer
}

Lifecycle

db.close(); // shuts down the gRPC channel

Filter syntax

Filters are plain JavaScript objects.

Field filter

{ field: 'age',  op: 'gt',       value: '30'    }
{ field: 'name', op: 'contains', value: 'alice' }
{ field: 'email',op: 'regex',    value: '.*@gmail\\.com' }

AND composite

{
  and: [
    { field: 'age',  op: 'gte', value: '18'    },
    { field: 'city', op: 'eq',  value: 'Berlin' },
  ],
}

OR composite

{
  or: [
    { field: 'role', op: 'eq', value: 'admin'     },
    { field: 'role', op: 'eq', value: 'superadmin' },
  ],
}

Supported op values

| op | Meaning | |------------|---------------------------| | eq | equal | | neq | not equal | | gt | greater than | | gte | greater than or equal | | lt | less than | | lte | less than or equal | | contains | string contains (substring)| | regex | regular expression match |


TLS

import * as fs from 'fs';

// From buffer
const db = new ScrivaDB('myserver.example.com', 5433, 'my-api-key',
  fs.readFileSync('/path/to/ca.crt'));

// From path (convenience static factory)
const db = ScrivaDB.fromTlsCertPath('myserver.example.com', 5433, 'my-api-key',
  '/path/to/ca.crt');

When no CA cert is supplied the client connects over plaintext (insecure channel).


Running the examples

Start the server first:

# From repo root
make run

Then in a separate terminal:

cd clients/js

# Install deps
npm install

# Basic CRUD example
npx ts-node examples/test_basic.ts

# Watch streaming example
npx ts-node examples/test_watch.ts

Unix socket

Node.js can connect over the Unix domain socket for zero-overhead local connections:

import * as grpc from '@grpc/grpc-js';
import { ScrivaDB } from 'scriva';

// Pass the socket path as a grpc URI:
//   'unix:///tmp/scriva.sock'
// Use the internal constructor signature with a pre-built stub for advanced use.

For the common case, TCP (localhost:5433) is sufficient. Unix socket support via a custom channel address is available using @grpc/grpc-js directly.