tr-pg-name-value-store 1.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 +18 -0
- package/README.md +66 -6
- package/dist/index.d.ts +1 -1
- package/dist/pg-name-value-store.d.ts +79 -2
- package/dist/pg-name-value-store.js +106 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
## [2.0.0] - 2026-09-07
|
|
8
|
+
|
|
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
|
+
|
|
7
24
|
## [1.0.0] - 2026-09-07
|
|
8
25
|
|
|
9
26
|
Initial release.
|
|
@@ -35,4 +52,5 @@ Initial release.
|
|
|
35
52
|
compose file for PostgreSQL 16 is included). Verified on PostgreSQL 9.6, 16
|
|
36
53
|
and 18.
|
|
37
54
|
|
|
55
|
+
[2.0.0]: https://github.com/rinne/node-tr-pg-name-value-store/releases/tag/v2.0.0
|
|
38
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,9 @@ 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.
|
|
13
16
|
- **Exact previous values.** Every write (`set`, `remove`, `update`) resolves to
|
|
14
17
|
the value it replaced. Writes to the same name are serialized across all
|
|
15
18
|
processes sharing the database, so that previous value is always the one left
|
|
@@ -73,7 +76,8 @@ A stored value is any JSON-serializable JavaScript value: `number`, `string`,
|
|
|
73
76
|
A name is any non-empty string of at most **1024 bytes of UTF-8** (so up to 1024
|
|
74
77
|
ASCII characters, fewer for non-ASCII) that does not contain `U+0000`. Anything
|
|
75
78
|
else throws `TypeError` before any I/O. Within those bounds names are opaque:
|
|
76
|
-
case-sensitive, whitespace-significant, any punctuation or Unicode.
|
|
79
|
+
case-sensitive, whitespace-significant, any punctuation or Unicode. Listing
|
|
80
|
+
orders them by byte order of their UTF-8 encoding (see [Listing](#listing)).
|
|
77
81
|
|
|
78
82
|
## API
|
|
79
83
|
|
|
@@ -144,6 +148,61 @@ Writes to other names, and reads of anything, are fine.
|
|
|
144
148
|
|
|
145
149
|
Removes every pair from this store's namespace. Other namespaces are untouched.
|
|
146
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
|
+
|
|
147
206
|
## Concurrency
|
|
148
207
|
|
|
149
208
|
Every write to a given name — `set`, `remove`, and `update` — runs in its own
|
|
@@ -155,10 +214,11 @@ the one before it. That is what makes the *previous value* contract exact and
|
|
|
155
214
|
what lets `update` callbacks compose (including the create-from-absent case,
|
|
156
215
|
which a row lock alone cannot cover).
|
|
157
216
|
|
|
158
|
-
Writes to different names never block each other. `get`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
162
222
|
|
|
163
223
|
Advisory-lock keys are derived in Node (SHA-256 of the namespace and name) — no
|
|
164
224
|
server-side extension is required.
|
|
@@ -180,7 +240,7 @@ same data and the same per-name serialization.
|
|
|
180
240
|
|
|
181
241
|
| Error | When |
|
|
182
242
|
|-----------------------|------|
|
|
183
|
-
| `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. |
|
|
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`). |
|
|
184
244
|
| `SchemaMismatchError` | A table with this namespace's name already exists with a different shape. It is left untouched. Exported by the package. |
|
|
185
245
|
| re-thrown value | `update` re-throws any non-`null`/`undefined` value its callback throws. |
|
|
186
246
|
|
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
|
*
|
|
@@ -39,8 +94,9 @@ export type UpdateCallback = (current: unknown) => unknown | Promise<unknown>;
|
|
|
39
94
|
* process sharing the database) by a per-(namespace, name) advisory lock. The
|
|
40
95
|
* *previous value* each of them resolves to is therefore exact: it is the value
|
|
41
96
|
* left by the immediately preceding write. Writes to different names never
|
|
42
|
-
* block each other. {@link get}
|
|
43
|
-
* latest committed
|
|
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.
|
|
44
100
|
*/
|
|
45
101
|
export declare class PgNameValueStore {
|
|
46
102
|
#private;
|
|
@@ -113,4 +169,25 @@ export declare class PgNameValueStore {
|
|
|
113
169
|
* are untouched.
|
|
114
170
|
*/
|
|
115
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>;
|
|
116
193
|
}
|
|
@@ -8,18 +8,51 @@ const schema_1 = require("./schema");
|
|
|
8
8
|
// violations fail fast with a TypeError instead of a server error. 1024 bytes is
|
|
9
9
|
// 1024 ASCII characters.
|
|
10
10
|
const NAME_MAX_BYTES = 1024;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Default and maximum page size of `list`. Larger requests clamp silently;
|
|
12
|
+
// `have_more` tells the caller to keep paging.
|
|
13
|
+
const MAX_LIST = 1000;
|
|
14
|
+
/**
|
|
15
|
+
* Validates a name-like string: `name` itself (non-empty) or a `prefix`/`after`
|
|
16
|
+
* listing argument (may be empty). Rejects non-strings, over-long strings (in
|
|
17
|
+
* UTF-8 bytes) and strings containing NUL, which PostgreSQL `text` cannot hold.
|
|
18
|
+
*/
|
|
19
|
+
function validateNameLike(value, what, allowEmpty) {
|
|
20
|
+
if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
|
|
21
|
+
throw new TypeError(allowEmpty ? `${what} must be a string` : `${what} must be a non-empty string`);
|
|
14
22
|
}
|
|
15
|
-
const bytes = Buffer.byteLength(
|
|
23
|
+
const bytes = Buffer.byteLength(value, 'utf8');
|
|
16
24
|
if (bytes > NAME_MAX_BYTES) {
|
|
17
|
-
throw new TypeError(
|
|
25
|
+
throw new TypeError(`${what} must be at most ${NAME_MAX_BYTES} bytes of UTF-8 (got ${bytes})`);
|
|
26
|
+
}
|
|
27
|
+
if (value.includes('\0')) {
|
|
28
|
+
throw new TypeError(`${what} must not contain U+0000`);
|
|
18
29
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
}
|
|
31
|
+
function validateName(name) {
|
|
32
|
+
validateNameLike(name, 'name', false);
|
|
33
|
+
}
|
|
34
|
+
/** Validates `ListOptions.limit`; returns the effective (clamped) page size. */
|
|
35
|
+
function validateLimit(limit) {
|
|
36
|
+
if (limit === undefined)
|
|
37
|
+
return MAX_LIST;
|
|
38
|
+
if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1) {
|
|
39
|
+
throw new TypeError('limit must be a positive integer');
|
|
22
40
|
}
|
|
41
|
+
return Math.min(limit, MAX_LIST);
|
|
42
|
+
}
|
|
43
|
+
function validateOptions(options, what) {
|
|
44
|
+
if (options === null || typeof options !== 'object') {
|
|
45
|
+
throw new TypeError(`${what} options must be an object`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Turns a literal prefix into a `LIKE` pattern: the pattern metacharacters
|
|
50
|
+
* `\`, `%` and `_` are escaped with PostgreSQL's default escape character
|
|
51
|
+
* (backslash), then `%` is appended. Bound as a parameter, so no SQL escaping
|
|
52
|
+
* is involved.
|
|
53
|
+
*/
|
|
54
|
+
function likePrefix(prefix) {
|
|
55
|
+
return prefix.replace(/[\\%_]/g, '\\$&') + '%';
|
|
23
56
|
}
|
|
24
57
|
/**
|
|
25
58
|
* Serializes a value to the JSON text bound (with an explicit `::jsonb` cast)
|
|
@@ -58,8 +91,9 @@ function serialize(value) {
|
|
|
58
91
|
* process sharing the database) by a per-(namespace, name) advisory lock. The
|
|
59
92
|
* *previous value* each of them resolves to is therefore exact: it is the value
|
|
60
93
|
* left by the immediately preceding write. Writes to different names never
|
|
61
|
-
* block each other. {@link get}
|
|
62
|
-
* latest committed
|
|
94
|
+
* block each other. Reads — {@link get}, {@link list}, {@link count} — are
|
|
95
|
+
* single lock-free statements that see the latest committed state and neither
|
|
96
|
+
* block writes nor are blocked by them.
|
|
63
97
|
*/
|
|
64
98
|
class PgNameValueStore {
|
|
65
99
|
#pool;
|
|
@@ -208,6 +242,68 @@ class PgNameValueStore {
|
|
|
208
242
|
await this.init();
|
|
209
243
|
await this.#pool.query(`DELETE FROM ${this.#table}`);
|
|
210
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Lists one page of names (with their values unless `values: false`) in
|
|
247
|
+
* ascending **byte order** of `name` (`COLLATE "C"`, so the order is the same
|
|
248
|
+
* on every database regardless of its locale). See {@link ListOptions}.
|
|
249
|
+
*
|
|
250
|
+
* Paging is keyset paging: pass the last `name` of a page as `after` to get
|
|
251
|
+
* the next one, while `have_more` is `true`. Each page is one lock-free
|
|
252
|
+
* statement over its own snapshot, so names written between two pages may or
|
|
253
|
+
* may not appear, and a name removed in between is simply absent.
|
|
254
|
+
*
|
|
255
|
+
* The primary-key index serves this query directly on databases whose
|
|
256
|
+
* default collation is `C`; elsewhere a page is a scan and sort of the
|
|
257
|
+
* store's names, which is fine for the sizes a per-service store reaches.
|
|
258
|
+
*/
|
|
259
|
+
async list(options = {}) {
|
|
260
|
+
validateOptions(options, 'list');
|
|
261
|
+
const { prefix = '', after = '', values = true } = options;
|
|
262
|
+
validateNameLike(prefix, 'prefix', true);
|
|
263
|
+
validateNameLike(after, 'after', true);
|
|
264
|
+
const limit = validateLimit(options.limit);
|
|
265
|
+
if (typeof values !== 'boolean') {
|
|
266
|
+
throw new TypeError('values must be a boolean');
|
|
267
|
+
}
|
|
268
|
+
await this.init();
|
|
269
|
+
const conds = [];
|
|
270
|
+
const params = [];
|
|
271
|
+
if (prefix !== '') {
|
|
272
|
+
params.push(likePrefix(prefix));
|
|
273
|
+
conds.push(`name LIKE $${params.length}`);
|
|
274
|
+
}
|
|
275
|
+
if (after !== '') {
|
|
276
|
+
params.push(after);
|
|
277
|
+
conds.push(`name COLLATE "C" > $${params.length}`);
|
|
278
|
+
}
|
|
279
|
+
params.push(limit + 1); // one extra row tells us whether more follow
|
|
280
|
+
const where = conds.length > 0 ? `WHERE ${conds.join(' AND ')}` : '';
|
|
281
|
+
const res = await this.#pool.query(`SELECT name${values ? ', v' : ''}, updated_at FROM ${this.#table}
|
|
282
|
+
${where}
|
|
283
|
+
ORDER BY name COLLATE "C"
|
|
284
|
+
LIMIT $${params.length}`, params);
|
|
285
|
+
const have_more = res.rows.length > limit;
|
|
286
|
+
const rows = have_more ? res.rows.slice(0, limit) : res.rows;
|
|
287
|
+
const entries = values
|
|
288
|
+
? rows.map((r) => ({ name: r.name, value: r.v, updated_at: r.updated_at }))
|
|
289
|
+
: rows.map((r) => ({ name: r.name, updated_at: r.updated_at }));
|
|
290
|
+
return { entries, have_more };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Resolves to the number of names in the store, or of those starting with
|
|
294
|
+
* `prefix` (same semantics as {@link ListOptions.prefix}). A single lock-free
|
|
295
|
+
* `SELECT count(*)`, which scans the matching names.
|
|
296
|
+
*/
|
|
297
|
+
async count(options = {}) {
|
|
298
|
+
validateOptions(options, 'count');
|
|
299
|
+
const { prefix = '' } = options;
|
|
300
|
+
validateNameLike(prefix, 'prefix', true);
|
|
301
|
+
await this.init();
|
|
302
|
+
const res = prefix === ''
|
|
303
|
+
? await this.#pool.query(`SELECT count(*)::int AS n FROM ${this.#table}`)
|
|
304
|
+
: await this.#pool.query(`SELECT count(*)::int AS n FROM ${this.#table} WHERE name LIKE $1`, [likePrefix(prefix)]);
|
|
305
|
+
return res.rows[0].n;
|
|
306
|
+
}
|
|
211
307
|
/**
|
|
212
308
|
* Runs `fn` inside a transaction on a dedicated pooled connection while
|
|
213
309
|
* holding the per-(namespace, name) transaction-scoped advisory lock, which
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-pg-name-value-store",
|
|
3
|
-
"version": "
|
|
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",
|