tr-pg-name-value-store
v0.0.0
Published
Persistent name/value store on PostgreSQL. JSONB values, self-maintaining never-migrated schema, atomic read-modify-write, multiple named stores per database.
Maintainers
Readme
tr-pg-name-value-store
A persistent name → value store backed by a single PostgreSQL table. A thin,
durable getter/setter: values are stored as JSONB, so a value may be anything
JSON can carry (number, string, boolean, object, array, or null) and is
returned as the corresponding JavaScript value.
- Self-maintaining schema. The table is created idempotently by the class itself on first use — no manual setup, no migrations. A pre-existing table is verified, never altered.
- Multiple named stores. A namespace gives each store its own table in the same database.
- Atomic read-modify-write.
update()runs a callback inside a transaction, serialized per name, so concurrent updates compose correctly. - Presence vs.
null. The store distinguishes no value (undefined) from the valuenull.
Install
npm install tr-pg-name-value-store pgpg is a peer dependency (>= 8). Requires Node >= 18 and PostgreSQL >= 9.5
(for INSERT … ON CONFLICT).
Quick start
import { Pool } from 'pg';
import { PgNameValueStore } from 'tr-pg-name-value-store';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const store = new PgNameValueStore(pool, { namespace: 'myapp' });
await store.set('greeting', 'hello');
await store.get('greeting'); // → 'hello'
await store.set('config', { retries: 3, tags: ['a', 'b'] });
await store.get('config'); // → { retries: 3, tags: ['a', 'b'] }
const previous = await store.set('greeting', 'hi'); // → 'hello' (the old value)
await store.remove('greeting'); // → 'hi' (the removed value)
await store.get('greeting'); // → undefinedValue model
A stored value is any JSON-serializable JavaScript value: number, string,
boolean, object, array, or null.
- Presence vs.
null. A name that has never been set (or has been removed) reads back asundefined. A name explicitly set to JSONnullreads back asnull. "The value does not exist" is always represented as the row being absent — there is no SQLNULLvalue. undefinedis not a value.set(name, undefined)throwsTypeError; useremove(name)to delete. (Insideupdate, a callback that returnsundefinedis the one placeundefinedis meaningful — it means delete.)- JSONB normalization. PostgreSQL normalizes JSONB on storage: object keys are reordered, insignificant whitespace is dropped, duplicate keys collapse to the last, and numbers are canonicalized. A value read back is semantically equal but may not be textually identical to the value written (e.g. key order).
API
new PgNameValueStore(pool, options?)
Constructs a store over a pg Pool. Performs no I/O. Options:
| Option | Type | Default | Meaning |
|-------------|----------|---------|---------|
| namespace | string | none | Backing table is <namespace>_name_value_store. Must match /^[a-z][a-z0-9_]*$/, ≤ 41 chars. Throws TypeError if invalid. Omit for the bare table name_value_store. |
init(): Promise<void>
Idempotently ensures the schema (creating the table if absent, verifying it if present). Called automatically on first use of any method; call it explicitly to surface schema/connection errors at startup. Safe to call repeatedly and concurrently (across processes too).
get(name): Promise<value>
Resolves to the current value, or undefined if name has no value.
set(name, value): Promise<previous>
Stores value (insert or overwrite); resolves to the previous value, or
undefined if there was none. Throws TypeError if value is undefined or
not JSON-serializable (writing nothing).
remove(name): Promise<previous>
Deletes name; resolves to its previous value, or undefined if it had none.
update(name, callback): Promise<previous>
Atomic read-modify-write. Fetches the current value, calls
callback(current) (awaiting a returned promise), then commits one outcome
based on what the callback does:
| Callback… | Effect | update… |
|--------------------------------------------------------|---------------------------------|------------------------------------|
| returns a JSON-serializable value (not undefined) | stores it | resolves to the previous value |
| returns undefined | removes the name | resolves to the previous value |
| returns a non-serializable value (function, BigInt, …) | nothing (rolled back) | throws TypeError |
| throws null or undefined | nothing (rolled back) | resolves to the previous value |
| throws anything else | nothing (rolled back) | re-throws that value unchanged |
The whole sequence runs in one transaction on a dedicated pooled connection. Concurrent updates of the same name are serialized by a per-name advisory lock, so each callback sees the committed result of the previous one — including the create-from-absent case.
// atomic counter (creates from absent, then increments)
await store.update('hits', (n) => (typeof n === 'number' ? n : 0) + 1);
// conditional update; abort with no change and no error
await store.update('config', (cfg) => {
if (!cfg) throw null; // graceful cancel
return { ...(cfg as object), seen: true };
});
// delete via update
await store.update('stale', () => undefined);removeAll(): Promise<void>
Removes every pair from this store's namespace. Other namespaces are untouched.
Multiple stores
Each namespace is an independent store with its own table:
const sessions = new PgNameValueStore(pool, { namespace: 'sessions' });
const settings = new PgNameValueStore(pool, { namespace: 'settings' });
// sessions.* and settings.* never collide; removeAll() on one leaves the other intactErrors
| Error | When |
|-----------------------|------|
| TypeError | Invalid namespace; invalid name (not a non-empty string, or > 1024 chars); a set value or update return that is not JSON-serializable. |
| SchemaMismatchError | A table with this namespace's name already exists with a different shape. It is left untouched. Exported by the package. |
| re-thrown value | update re-throws any non-null/undefined value its callback throws. |
Operational/connection failures propagate from the underlying pg calls.
Schema
One table per namespace ({{ns}} is <namespace>_, or empty):
CREATE TABLE {{ns}}name_value_store (
name TEXT NOT NULL,
v JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (name)
);The module issues only CREATE TABLE IF NOT EXISTS and verifies the columns of
a pre-existing table against this shape — it never runs ALTER or DROP.
License
MIT © Timo J. Rinne <[email protected]>
