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

@johnhenry/objectify

v0.0.2

Published

TypeScript adapter for objectify — persistent, versioned JSON objects backed by SQLite

Readme

@johnhenry/objectify

npm version

TypeScript adapter for objectify — persistent, versioned JSON objects backed by SQLite.

Talks directly to the same SQLite database as the objectify CLI. No shelling out, no server process. Use it to give your Node.js apps versioned, typed state — or as the state layer behind an RPC framework like oRPC.

Provenance: this package was previously an internal, unpublished package ([email protected], workspace-local only — it never had a version on the npm registry). It is published as @johnhenry/objectify, restarting at 0.0.0 per the @johnhenry adoption convention -- briefly published as @johnhenry/objectify-js first, corrected within the hour once it was clear the plain objectify name (reserved for the CLI's own npm install shim until then) was the right fit for this package instead.

Installation

npm install @johnhenry/objectify

Requires Node.js 26+ — the @johnhenry family-wide minimum-supported-version policy, not a constraint from this package's own dependencies: better-sqlite3 itself only requires Node 22+, and its N-API prebuilds are what let this package install without a native compiler toolchain either way.

Requires an initialized objectify store. If you haven't already:

# install the CLI (prebuilt binary, no Rust toolchain required)
npm install -g @johnhenry/objectify-cli

# create a store
objectify init

Quick start

import { Objectify } from '@johnhenry/objectify';

const store = new Objectify(); // auto-finds .objectify/ walking up from cwd

// Create an object
const id = store.create({ description: 'my config' });

// Get a handle to it
const ref = store.use(id);

// Read and write state
ref.set({ theme: 'dark', fontSize: 14 });
ref.get(); // → { theme: 'dark', fontSize: 14 }

// Every write is versioned
ref.set({ theme: 'light', fontSize: 14 });
ref.log();
// → [
//   { version: 1, method: 'create', at: '...' },
//   { version: 2, method: 'set', at: '...' },
//   { version: 3, method: 'set', at: '...' },
// ]

// Rewind to any version
ref.rewind(2); // non-destructive, creates version 4

// Diff between versions
ref.diff(2, 3); // → RFC 6902 JSON Patch

// Fork into an independent copy
const forkId = ref.fork();

store.close();

Class methods

If the object has a class (a .ts or .py file in .objectify/classes/), you can call its methods:

const id = store.create({ description: 'sprint tasks', class: 'TaskList' });
const tasks = store.use(id);

tasks.call('add', { title: 'write tests' });
// → { id: '...', title: 'write tests', done: false, createdAt: '...' }

tasks.call('pending');
// → [{ id: '...', title: 'write tests', done: false }]

tasks.call('complete', { id: '...' });
tasks.call('done');
// → [{ id: '...', title: 'write tests', done: true }]

Class methods execute via Deno (TypeScript) or Python 3 — the same runtimes the CLI uses. State is validated against the class schema on every write.

API reference

Objectify

const store = new Objectify(opts?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | dir | string | auto-detect | Path to .objectify/ directory |

Methods:

| Method | Returns | Description | |--------|---------|-------------| | create(opts?) | string | Create an object, returns short ID | | destroy(idPrefix) | void | Permanently delete an object | | use(idPrefix) | ObjectRef | Get a handle to an object | | inspect(idPrefix) | InspectResult | Object metadata | | list(opts?) | InspectResult[] | List objects with optional filters | | gc() | number | Delete expired objects, returns count | | close() | void | Close the database connection |

create options:

| Option | Type | Description | |--------|------|-------------| | description | string | Human-readable label | | class | string | Class name (must exist as .ts or .py in classes/) | | expire | string | TTL duration (30s, 15m, 2h, 7d, 2w) |

list options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | class | string | — | Filter by class name | | expired | boolean | false | Include expired objects | | since | string | — | ISO date or relative duration | | limit | number | 50 | Max results | | offset | number | 0 | Pagination offset |

ObjectRef

Returned by store.use(idPrefix). All operations are scoped to a single resolved object.

| Method | Returns | Description | |--------|---------|-------------| | get(version?) | T | Current state, or state at a specific version | | set(state) | void | Replace state (validates against schema) | | call(method, input?) | unknown | Call a class method | | log() | LogEntry[] | Version history | | diff(v1, v2) | Operation[] | RFC 6902 JSON Patch between two versions | | rewind(version) | { rewoundTo, newVersion } | Restore to a previous version | | fork(opts?) | string | Create an independent copy, returns short ID | | inspect() | InspectResult | Object metadata |

Properties:

| Property | Type | Description | |----------|------|-------------| | id | string | Full 32-character hex ID | | shortId | string | Minimum unique prefix |


Using with oRPC

objectify is a natural fit as the state layer behind oRPC procedures. oRPC handles the network boundary (HTTP, type safety, OpenAPI spec generation), while objectify handles the state boundary (persistence, versioning, schema validation). Together, you get typed RPC endpoints with built-in version history, rollback, and audit trails — without writing any state management code.

Basic setup

import { Objectify } from '@johnhenry/objectify';
import { os } from '@orpc/server';
import { z } from 'zod';

// One store, shared across all routes
const store = new Objectify();

A task list API

Assume you have a TaskList class in .objectify/classes/TaskList.ts with methods add, complete, pending, and done.

// Create the object once (or look up an existing one)
const TASKS_ID = store.create({ description: 'sprint tasks', class: 'TaskList' });
const tasks = store.use(TASKS_ID);

const taskRouter = {
  // Each class method becomes an oRPC procedure
  add: os
    .input(z.object({ title: z.string() }))
    .handler(({ input }) => tasks.call('add', input)),

  complete: os
    .input(z.object({ id: z.string() }))
    .handler(({ input }) => tasks.call('complete', input)),

  pending: os
    .handler(() => tasks.call('pending')),

  done: os
    .handler(() => tasks.call('done')),

  // Version history and rollback come free from objectify
  history: os
    .handler(() => tasks.log()),

  diff: os
    .input(z.object({ v1: z.number(), v2: z.number() }))
    .handler(({ input }) => tasks.diff(input.v1, input.v2)),

  rewind: os
    .input(z.object({ version: z.number() }))
    .handler(({ input }) => tasks.rewind(input.version)),
};

That's it. Every procedure gets automatic OpenAPI docs via oRPC, and every state mutation gets automatic versioning via objectify. The client gets type-safe calls:

const tasks = await orpc.tasks.pending();
const task = await orpc.tasks.add({ title: 'write tests' });
await orpc.tasks.complete({ id: task.id });

// Audit and recover
const history = await orpc.tasks.history();
const changes = await orpc.tasks.diff({ v1: 1, v2: 5 });
await orpc.tasks.rewind({ version: 3 });

Multiple instances

objectify's instance model means each create produces an independent object. This maps naturally to multi-tenant or per-resource APIs:

const projectRouter = {
  create: os
    .input(z.object({ name: z.string() }))
    .handler(({ input }) => {
      const id = store.create({
        description: input.name,
        class: 'ProjectBoard',
      });
      return { id };
    }),

  use: os
    .input(z.object({ id: z.string(), method: z.string(), input: z.any().optional() }))
    .handler(({ input }) => {
      const ref = store.use(input.id);
      return ref.call(input.method, input.input);
    }),
};

Each project gets its own versioned state, its own history, and its own rollback capabilities — with no additional infrastructure.

What this gives you that oRPC alone doesn't

| Concern | oRPC alone | oRPC + objectify | |---------|-----------|-----------------| | Type-safe client calls | Yes | Yes | | OpenAPI spec | Yes | Yes | | Persistent state | You build it | Built-in | | Version history | You build it | tasks.log() | | Rollback | You build it | tasks.rewind(n) | | Audit diff | You build it | tasks.diff(a, b) | | Fork state | You build it | tasks.fork() | | Schema validation | Zod at the edge | Zod + JSON Schema on state | | Multiple instances | You add routing | store.create() |

The objectify class defines the business logic once. oRPC exposes it over HTTP. Neither needs to know about the other's internals.


Shared database with the CLI

The adapter reads and writes the same objectify.db file as the objectify CLI. This means you can:

  • Create objects from the CLI, use them from Node.js (or vice versa)
  • Inspect state with objectify log / objectify diff while your server is running
  • Use shell aliases alongside your API: alias tasks="objectify use 3fa8"

SQLite WAL mode ensures safe concurrent reads between the CLI and the adapter. Writes are serialized by SQLite's locking — this is fine for typical usage but not designed for high-concurrency write workloads.

License

MIT