tr-pg-name-value-store 0.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,20 +4,53 @@ All notable changes to this project are documented in this file. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.0.0] - Unreleased
7
+ ## [2.0.0] - 2026-09-07
8
8
 
9
- Initial implementation, ready for testing.
9
+ No breaking changes; every existing method, guarantee and the frozen schema are
10
+ untouched.
11
+
12
+ ### Added
13
+
14
+ - `list(options?)`: pages through the store's names — with their values, or
15
+ without (`values: false`) for a cheap walk over large documents — in byte
16
+ order of the name (`COLLATE "C"`, identical on every database), with keyset
17
+ paging (`after`, `have_more`, `limit` up to 1000) and a literal `prefix`
18
+ filter in which `%`, `_` and `\` match themselves. Each page is one
19
+ lock-free statement over its own snapshot; no index is added.
20
+ - `count(options?)`: the number of names, optionally under the same literal
21
+ prefix.
22
+ - Exported types `ListOptions`, `ListEntry`, `ListResult` and `CountOptions`.
23
+
24
+ ## [1.0.0] - 2026-09-07
25
+
26
+ Initial release.
10
27
 
11
28
  ### Added
12
29
 
13
30
  - `PgNameValueStore` class constructed from a pg `Pool`, with a self-maintaining,
14
31
  never-migrated schema (one `name_value_store` table per namespace; pre-existing
15
- tables are verified, never altered).
32
+ tables are verified, never altered; a matching operator-created table is
33
+ adopted as-is).
16
34
  - Async API: `init`, `get`, `set`, `remove`, `update`, `removeAll`.
17
35
  - JSONB value storage for any JSON value; `undefined` (no value) is kept distinct
18
36
  from a stored JSON `null` via row presence.
19
- - Atomic `update(name, callback)` read-modify-write, serialized per name by a
20
- PostgreSQL advisory lock, with graceful cancel (callback throws `null`/
21
- `undefined`), delete (callback returns `undefined`), and error pass-through.
22
- - `SchemaMismatchError` for a conflicting pre-existing table.
23
- - Integration test suite (vitest) and documentation.
37
+ - Exact previous-value contract: `set`, `remove` and `update` each resolve to the
38
+ value they replaced. All writes to the same name run in a transaction on a
39
+ dedicated connection under a per-`(namespace, name)` advisory lock, so they are
40
+ strictly serialized across processes and the previous value is always the one
41
+ left by the immediately preceding write. Writes to different names, and all
42
+ reads, are never blocked.
43
+ - Atomic `update(name, callback)` read-modify-write with graceful cancel
44
+ (callback throws `null`/`undefined`), delete (callback returns `undefined`),
45
+ and error pass-through.
46
+ - Name validation: a non-empty string of at most 1024 bytes of UTF-8 without
47
+ `U+0000`, rejected with `TypeError` before any I/O.
48
+ - `SchemaMismatchError` for a conflicting pre-existing table. A failed `init()`
49
+ is not cached; the next call retries.
50
+ - Integration test suite (vitest) against a real PostgreSQL — a throwaway local
51
+ cluster by default, or any server via `TR_PG_NAME_VALUE_STORE_TEST_URL` (a
52
+ compose file for PostgreSQL 16 is included). Verified on PostgreSQL 9.6, 16
53
+ and 18.
54
+
55
+ [2.0.0]: https://github.com/rinne/node-tr-pg-name-value-store/releases/tag/v2.0.0
56
+ [1.0.0]: https://github.com/rinne/node-tr-pg-name-value-store/releases/tag/v1.0.0
package/README.md CHANGED
@@ -10,6 +10,13 @@ returned as the corresponding JavaScript value.
10
10
  *verified, never altered*.
11
11
  - **Multiple named stores.** A namespace gives each store its own table in the
12
12
  same database.
13
+ - **Listing.** `list()` pages through the names (with values, or without for a
14
+ cheap walk) in byte order with keyset paging, and `count()` counts them, both
15
+ optionally under a literal prefix — so nobody needs to query the table.
16
+ - **Exact previous values.** Every write (`set`, `remove`, `update`) resolves to
17
+ the value it replaced. Writes to the same name are serialized across all
18
+ processes sharing the database, so that previous value is always the one left
19
+ by the immediately preceding write.
13
20
  - **Atomic read-modify-write.** `update()` runs a callback inside a transaction,
14
21
  serialized per name, so concurrent updates compose correctly.
15
22
  - **Presence vs. `null`.** The store distinguishes *no value* (`undefined`) from
@@ -22,7 +29,8 @@ npm install tr-pg-name-value-store pg
22
29
  ```
23
30
 
24
31
  `pg` is a peer dependency (`>= 8`). Requires Node `>= 18` and PostgreSQL `>= 9.5`
25
- (for `INSERT … ON CONFLICT`).
32
+ (for `INSERT … ON CONFLICT`). The test suite runs against PostgreSQL 9.6, 16 and
33
+ 18.
26
34
 
27
35
  ## Quick start
28
36
 
@@ -60,6 +68,16 @@ A stored value is any JSON-serializable JavaScript value: `number`, `string`,
60
68
  are reordered, insignificant whitespace is dropped, duplicate keys collapse to
61
69
  the last, and numbers are canonicalized. A value read back is *semantically*
62
70
  equal but may not be textually identical to the value written (e.g. key order).
71
+ - **No `U+0000` in strings.** PostgreSQL's `jsonb` cannot store the NUL character
72
+ inside a string; the server's error propagates unchanged and nothing is written.
73
+
74
+ ## Names
75
+
76
+ A name is any non-empty string of at most **1024 bytes of UTF-8** (so up to 1024
77
+ ASCII characters, fewer for non-ASCII) that does not contain `U+0000`. Anything
78
+ else throws `TypeError` before any I/O. Within those bounds names are opaque:
79
+ case-sensitive, whitespace-significant, any punctuation or Unicode. Listing
80
+ orders them by byte order of their UTF-8 encoding (see [Listing](#listing)).
63
81
 
64
82
  ## API
65
83
 
@@ -76,11 +94,13 @@ Constructs a store over a pg `Pool`. Performs no I/O. Options:
76
94
  Idempotently ensures the schema (creating the table if absent, verifying it if
77
95
  present). Called automatically on first use of any method; call it explicitly to
78
96
  surface schema/connection errors at startup. Safe to call repeatedly and
79
- concurrently (across processes too).
97
+ concurrently (across processes too). A failed `init()` is not cached; the next
98
+ call retries.
80
99
 
81
100
  ### `get(name): Promise<value>`
82
101
 
83
- Resolves to the current value, or `undefined` if `name` has no value.
102
+ Resolves to the current value, or `undefined` if `name` has no value. A single
103
+ lock-free `SELECT` that reads the latest committed value.
84
104
 
85
105
  ### `set(name, value): Promise<previous>`
86
106
 
@@ -103,14 +123,9 @@ based on what the callback does:
103
123
  | returns a JSON-serializable value (not `undefined`) | stores it | resolves to the previous value |
104
124
  | returns `undefined` | removes the name | resolves to the previous value |
105
125
  | returns a non-serializable value (function, `BigInt`, …) | nothing (rolled back) | throws `TypeError` |
106
- | throws `null` or `undefined` | nothing (rolled back) | resolves to the previous value |
126
+ | throws `null` or `undefined` | nothing (graceful cancel) | resolves to the previous value |
107
127
  | throws anything else | nothing (rolled back) | re-throws that value unchanged |
108
128
 
109
- The whole sequence runs in one transaction on a dedicated pooled connection.
110
- Concurrent updates of the **same** name are serialized by a per-name advisory
111
- lock, so each callback sees the committed result of the previous one — including
112
- the create-from-absent case.
113
-
114
129
  ```ts
115
130
  // atomic counter (creates from absent, then increments)
116
131
  await store.update('hits', (n) => (typeof n === 'number' ? n : 0) + 1);
@@ -125,10 +140,89 @@ await store.update('config', (cfg) => {
125
140
  await store.update('stale', () => undefined);
126
141
  ```
127
142
 
143
+ The callback must not write to the **same name** through this or any other
144
+ store instance — it would wait forever on the lock the callback itself holds.
145
+ Writes to other names, and reads of anything, are fine.
146
+
128
147
  ### `removeAll(): Promise<void>`
129
148
 
130
149
  Removes every pair from this store's namespace. Other namespaces are untouched.
131
150
 
151
+ ### Listing
152
+
153
+ #### `list(options?): Promise<ListResult>`
154
+
155
+ Lists one page of names, in ascending **byte order** of the name, with their
156
+ values unless `values: false`.
157
+
158
+ | Option | Type | Default | Meaning |
159
+ |----------|-----------|---------|---------|
160
+ | `prefix` | `string` | none | Only names starting with this exact string. Literal, not a pattern: `%`, `_` and `\` match themselves. Validated like a name (≤ 1024 bytes, no `U+0000`); empty means no filter. |
161
+ | `after` | `string` | none | Keyset cursor: only names strictly after this one in byte order — normally the last name of the previous page. Need not be an existing name. Validated like `prefix`; empty means start from the beginning. |
162
+ | `limit` | `number` | 1000 | Page size, a positive integer. The maximum is 1000; larger values clamp silently (keep paging while `have_more` is true). A non-integer or a value below 1 throws `TypeError`. |
163
+ | `values` | `boolean` | `true` | With `false`, `value` is omitted from every entry and the value column is not read, so a store of large documents can be walked cheaply. |
164
+
165
+ The result:
166
+
167
+ | Field | Type | Meaning |
168
+ |-------------|---------------|---------|
169
+ | `entries` | `ListEntry[]` | The page, each `{ name, value?, updated_at }`. `value` is present (and may be `null`) whenever `values` is on; a name with no value has no entry at all. `updated_at` is a `Date` (millisecond precision) of the last write. |
170
+ | `have_more` | `boolean` | `true` if more names followed this page in the same snapshot; continue with `after` set to the last entry's name. |
171
+
172
+ Paging loop:
173
+
174
+ ```ts
175
+ const names: string[] = [];
176
+ let after: string | undefined;
177
+ for (;;) {
178
+ const page = await store.list({ prefix: 'job:', after, values: false });
179
+ names.push(...page.entries.map((e) => e.name));
180
+ if (!page.have_more) break;
181
+ after = page.entries.at(-1)!.name; // a page with have_more is never empty
182
+ }
183
+ ```
184
+
185
+ Each page is one lock-free statement over its own snapshot. Names written
186
+ between two pages may or may not appear, and a name removed in between is
187
+ simply absent (so `have_more: true` followed by an empty page is possible).
188
+ There are no offsets: a name that exists for the whole walk is returned exactly
189
+ once.
190
+
191
+ Ordering is byte order of the UTF-8 name (`COLLATE "C"`), identical on every
192
+ database regardless of its locale — so `'B' < '_x' < 'a'`. It differs from
193
+ JavaScript's default string comparison only for characters outside the Basic
194
+ Multilingual Plane, which matters if you merge pages with locally sorted data.
195
+ On databases whose default collation is `C` the primary-key index serves
196
+ `after` and `prefix` directly; on other databases each page is a scan and sort
197
+ of the store's names, which is fine for the sizes a per-service store reaches.
198
+ No index is added — the schema stays frozen.
199
+
200
+ #### `count(options?): Promise<number>`
201
+
202
+ Resolves to the number of names in the store, or of those starting with
203
+ `options.prefix` (same semantics as above). A single lock-free statement that
204
+ scans the matching names.
205
+
206
+ ## Concurrency
207
+
208
+ Every write to a given name — `set`, `remove`, and `update` — runs in its own
209
+ transaction on a dedicated pooled connection and holds a per-`(namespace, name)`
210
+ transaction-scoped advisory lock (`pg_advisory_xact_lock`) for its duration.
211
+ Writes to the same name are therefore strictly serialized, across every process
212
+ that shares the database, and each one observes exactly the committed result of
213
+ the one before it. That is what makes the *previous value* contract exact and
214
+ what lets `update` callbacks compose (including the create-from-absent case,
215
+ which a row lock alone cannot cover).
216
+
217
+ Writes to different names never block each other. Reads — `get`, `list` and
218
+ `count` — are single lock-free statements that see the latest committed state
219
+ and neither block writes nor are blocked by them. `removeAll` is a single bulk
220
+ `DELETE` that does not take per-name locks; it waits for in-flight writes on
221
+ individual rows like any other statement would.
222
+
223
+ Advisory-lock keys are derived in Node (SHA-256 of the namespace and name) — no
224
+ server-side extension is required.
225
+
132
226
  ## Multiple stores
133
227
 
134
228
  Each namespace is an independent store with its own table:
@@ -139,15 +233,20 @@ const settings = new PgNameValueStore(pool, { namespace: 'settings' });
139
233
  // sessions.* and settings.* never collide; removeAll() on one leaves the other intact
140
234
  ```
141
235
 
236
+ Several instances over the same namespace (in one process or many) share the
237
+ same data and the same per-name serialization.
238
+
142
239
  ## Errors
143
240
 
144
241
  | Error | When |
145
242
  |-----------------------|------|
146
- | `TypeError` | Invalid namespace; invalid name (not a non-empty string, or > 1024 chars); a `set` value or `update` return that is not JSON-serializable. |
243
+ | `TypeError` | Invalid namespace; invalid name (not a non-empty string, more than 1024 bytes of UTF-8, or containing `U+0000`); a `set` value or `update` return that is not JSON-serializable; an `update` callback that is not a function; invalid `list`/`count` options (a `prefix` or `after` that is not a string within the name bounds, a `limit` that is not a positive integer, a non-boolean `values`). |
147
244
  | `SchemaMismatchError` | A table with this namespace's name already exists with a different shape. It is left untouched. Exported by the package. |
148
245
  | re-thrown value | `update` re-throws any non-`null`/`undefined` value its callback throws. |
149
246
 
150
247
  Operational/connection failures propagate from the underlying `pg` calls.
248
+ Argument validation errors are thrown synchronously by the constructor and as
249
+ rejected promises by every other method, always before any I/O.
151
250
 
152
251
  ## Schema
153
252
 
@@ -163,7 +262,32 @@ CREATE TABLE {{ns}}name_value_store (
163
262
  ```
164
263
 
165
264
  The module issues only `CREATE TABLE IF NOT EXISTS` and verifies the columns of
166
- a pre-existing table against this shape — it never runs `ALTER` or `DROP`.
265
+ a pre-existing table against this shape — it never runs `ALTER` or `DROP`. A
266
+ matching table created by an operator (with any existing rows) is adopted as-is.
267
+ `updated_at` is maintained on every write for operational inspection; it has no
268
+ public accessor.
269
+
270
+ ## Development
271
+
272
+ ```sh
273
+ npm install
274
+ npm test # embeds sql/, typechecks src + tests, runs vitest
275
+ npm run build # emits dist/
276
+ npm pack --dry-run # audit the tarball contents before publishing
277
+ ```
278
+
279
+ The tests run against a real PostgreSQL. By default a throwaway cluster is
280
+ bootstrapped with `initdb`/`pg_ctl` in a temp directory (PostgreSQL server
281
+ binaries must be on `PATH`) and torn down afterwards. To use an existing server
282
+ instead — for example the bundled compose file — point
283
+ `TR_PG_NAME_VALUE_STORE_TEST_URL` at an admin connection; a scratch database
284
+ named `tr_pg_name_value_store_test` is (re)created on it:
285
+
286
+ ```sh
287
+ docker compose -f test/docker-compose.yml up -d
288
+ TR_PG_NAME_VALUE_STORE_TEST_URL=postgres://postgres:postgres@localhost:5434/postgres npm test
289
+ docker compose -f test/docker-compose.yml down
290
+ ```
167
291
 
168
292
  ## License
169
293
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { PgNameValueStore, type PgNameValueStoreOptions, type UpdateCallback, } from './pg-name-value-store';
1
+ export { PgNameValueStore, type PgNameValueStoreOptions, type UpdateCallback, type ListOptions, type ListEntry, type ListResult, type CountOptions, } from './pg-name-value-store';
2
2
  export { SchemaMismatchError } from './schema';
@@ -21,6 +21,61 @@ export interface PgNameValueStoreOptions {
21
21
  * - return a non-serializable value → cancel (no change), a `TypeError` is thrown.
22
22
  */
23
23
  export type UpdateCallback = (current: unknown) => unknown | Promise<unknown>;
24
+ /** Options for {@link PgNameValueStore.list}. */
25
+ export interface ListOptions {
26
+ /**
27
+ * Only names that start with this exact string. Literal, not a pattern: `%`,
28
+ * `_` and `\` in the prefix match themselves. Validated like a name (at most
29
+ * 1024 bytes of UTF-8, no `U+0000`). Empty or omitted: no filter.
30
+ */
31
+ prefix?: string;
32
+ /**
33
+ * Keyset cursor: only names strictly after this one in byte order — normally
34
+ * the `name` of the last entry of the previous page. Need not be an existing
35
+ * name. Validated like `prefix`. Empty or omitted: start from the first name.
36
+ */
37
+ after?: string;
38
+ /**
39
+ * Page size. A positive integer; default and maximum 1000 (larger values are
40
+ * clamped silently — keep paging while `have_more` is true). A non-integer or
41
+ * a value below 1 throws `TypeError` before any I/O.
42
+ */
43
+ limit?: number;
44
+ /**
45
+ * Whether to include values (default `true`). With `false` the `value` field
46
+ * is omitted from every entry and the value column is not read at all, so a
47
+ * store of large documents can be walked cheaply.
48
+ */
49
+ values?: boolean;
50
+ }
51
+ /** One entry of a {@link ListResult}. */
52
+ export interface ListEntry {
53
+ /** The name. */
54
+ name: string;
55
+ /**
56
+ * The stored value — present (possibly `null`) whenever the page was listed
57
+ * with `values` on; omitted entirely with `values: false`. A name with no
58
+ * value has no entry at all, so this is never `undefined` when present.
59
+ */
60
+ value?: unknown;
61
+ /** When the value was last written (millisecond precision). */
62
+ updated_at: Date;
63
+ }
64
+ /** Result of {@link PgNameValueStore.list}. */
65
+ export interface ListResult {
66
+ /** The page, in ascending byte order of `name`. */
67
+ entries: ListEntry[];
68
+ /**
69
+ * `true` if more names followed this page in the same snapshot; continue with
70
+ * `after: entries.at(-1).name`.
71
+ */
72
+ have_more: boolean;
73
+ }
74
+ /** Options for {@link PgNameValueStore.count}. */
75
+ export interface CountOptions {
76
+ /** Same semantics as {@link ListOptions.prefix}. */
77
+ prefix?: string;
78
+ }
24
79
  /**
25
80
  * A persistent name → value store backed by a single PostgreSQL table.
26
81
  *
@@ -32,6 +87,16 @@ export type UpdateCallback = (current: unknown) => unknown | Promise<unknown>;
32
87
  * The constructor takes a pg `Pool`; the schema (one table per namespace) is
33
88
  * created automatically and idempotently on first use, or via an explicit
34
89
  * {@link init} call. An existing table is never altered — only verified.
90
+ *
91
+ * **Concurrency.** Every write to a given name — {@link set}, {@link remove},
92
+ * and {@link update} — runs in its own transaction on a dedicated pooled
93
+ * connection, serialized with all other writes to the same name (in any
94
+ * process sharing the database) by a per-(namespace, name) advisory lock. The
95
+ * *previous value* each of them resolves to is therefore exact: it is the value
96
+ * left by the immediately preceding write. Writes to different names never
97
+ * block each other. Reads — {@link get}, {@link list}, {@link count} — are
98
+ * single lock-free statements that see the latest committed state and neither
99
+ * block writes nor are blocked by them.
35
100
  */
36
101
  export declare class PgNameValueStore {
37
102
  #private;
@@ -54,7 +119,7 @@ export declare class PgNameValueStore {
54
119
  /**
55
120
  * Resolves to the current value of `name`, or `undefined` if `name` has no
56
121
  * value. A name explicitly set to JSON `null` resolves to `null` (distinct
57
- * from `undefined`).
122
+ * from `undefined`). A single lock-free `SELECT`.
58
123
  */
59
124
  get(name: string): Promise<unknown>;
60
125
  /**
@@ -62,14 +127,15 @@ export declare class PgNameValueStore {
62
127
  * resolves to the **previous** value, or `undefined` if there was none.
63
128
  *
64
129
  * `value` must be JSON-serializable and must not be `undefined`; otherwise
65
- * `set` throws `TypeError` and writes nothing. Performed as a single
66
- * statement, so it is atomic.
130
+ * `set` throws `TypeError` and writes nothing. Serialized with every other
131
+ * write to the same name (see the class docs), so the previous value is
132
+ * exactly the value left by the preceding write.
67
133
  */
68
134
  set(name: string, value: unknown): Promise<unknown>;
69
135
  /**
70
136
  * Removes `name` from the store and resolves to its previous value, or
71
137
  * `undefined` if `name` had no value. Removing an absent name is a no-op that
72
- * resolves to `undefined`. A single `DELETE RETURNING`; atomic.
138
+ * resolves to `undefined`. Serialized with every other write to the same name.
73
139
  */
74
140
  remove(name: string): Promise<unknown>;
75
141
  /**
@@ -78,19 +144,23 @@ export declare class PgNameValueStore {
78
144
  *
79
145
  * - `cb` returns a JSON-serializable value (not `undefined`) → store it;
80
146
  * - `cb` returns `undefined` → remove `name`;
81
- * - `cb` returns a non-serializable value → roll back, throw `TypeError`;
82
- * - `cb` throws `null`/`undefined` → roll back (graceful cancel), no error;
83
- * - `cb` throws anything else → roll back, re-throw that value unchanged.
147
+ * - `cb` returns a non-serializable value → no change, throw `TypeError`;
148
+ * - `cb` throws `null`/`undefined` → no change (graceful cancel), no error;
149
+ * - `cb` throws anything else → no change, re-throw that value unchanged.
84
150
  *
85
151
  * On every non-throwing outcome (store, remove, or graceful cancel) `update`
86
152
  * resolves to the value that was in the database **before** the call (or
87
153
  * `undefined` if there was none).
88
154
  *
89
155
  * The whole sequence runs in one transaction on a dedicated pooled
90
- * connection. Concurrent updates of the same `name` are serialized by a
91
- * per-(namespace, name) transaction-scoped advisory lock, so each callback
92
- * sees the committed result of the previous update — including the
93
- * create-from-absent case. An existing row is additionally taken `FOR UPDATE`.
156
+ * connection, serialized with every other write to the same name by the
157
+ * per-(namespace, name) advisory lock, so each callback sees the committed
158
+ * result of the previous write — including the create-from-absent case. An
159
+ * existing row is additionally taken `FOR UPDATE`.
160
+ *
161
+ * The callback must not write to the **same name** through this or any other
162
+ * store instance (it would wait on the lock the callback itself holds); writes
163
+ * to other names are fine.
94
164
  */
95
165
  update(name: string, cb: UpdateCallback): Promise<unknown>;
96
166
  /**
@@ -99,4 +169,25 @@ export declare class PgNameValueStore {
99
169
  * are untouched.
100
170
  */
101
171
  removeAll(): Promise<void>;
172
+ /**
173
+ * Lists one page of names (with their values unless `values: false`) in
174
+ * ascending **byte order** of `name` (`COLLATE "C"`, so the order is the same
175
+ * on every database regardless of its locale). See {@link ListOptions}.
176
+ *
177
+ * Paging is keyset paging: pass the last `name` of a page as `after` to get
178
+ * the next one, while `have_more` is `true`. Each page is one lock-free
179
+ * statement over its own snapshot, so names written between two pages may or
180
+ * may not appear, and a name removed in between is simply absent.
181
+ *
182
+ * The primary-key index serves this query directly on databases whose
183
+ * default collation is `C`; elsewhere a page is a scan and sort of the
184
+ * store's names, which is fine for the sizes a per-service store reaches.
185
+ */
186
+ list(options?: ListOptions): Promise<ListResult>;
187
+ /**
188
+ * Resolves to the number of names in the store, or of those starting with
189
+ * `prefix` (same semantics as {@link ListOptions.prefix}). A single lock-free
190
+ * `SELECT count(*)`, which scans the matching names.
191
+ */
192
+ count(options?: CountOptions): Promise<number>;
102
193
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-pg-name-value-store",
3
- "version": "0.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Persistent name/value store on PostgreSQL. JSONB values, self-maintaining never-migrated schema, atomic read-modify-write, multiple named stores per database.",
5
5
  "keywords": [
6
6
  "name-value",
@@ -29,7 +29,8 @@
29
29
  "embed-sql": "node scripts/embed-sql.mjs",
30
30
  "prebuild": "npm run embed-sql",
31
31
  "build": "tsc -p tsconfig.json",
32
- "pretest": "npm run embed-sql",
32
+ "typecheck": "tsc -p tsconfig.test.json",
33
+ "pretest": "npm run embed-sql && npm run typecheck",
33
34
  "test": "vitest run",
34
35
  "prepack": "npm run build"
35
36
  },
@@ -37,11 +38,11 @@
37
38
  "pg": ">=8"
38
39
  },
39
40
  "devDependencies": {
40
- "@types/node": "^20.0.0",
41
- "@types/pg": "^8.10.0",
42
- "pg": "^8.11.0",
43
- "typescript": "^5.4.0",
44
- "vitest": "^4.1.8"
41
+ "@types/node": "^20.19.0",
42
+ "@types/pg": "^8.23.0",
43
+ "pg": "^8.23.0",
44
+ "typescript": "^5.9.0",
45
+ "vitest": "^4.1.0"
45
46
  },
46
47
  "repository": {
47
48
  "type": "git",