@keyv/postgres 2.2.2 → 6.0.0-alpha.1
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/README.md +434 -23
- package/dist/index.cjs +596 -102
- package/dist/index.d.cts +275 -15
- package/dist/index.d.ts +275 -15
- package/dist/index.js +596 -102
- package/package.json +11 -9
package/dist/index.js
CHANGED
|
@@ -1,161 +1,654 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import { Hookified } from "hookified";
|
|
3
3
|
import Keyv from "keyv";
|
|
4
4
|
|
|
5
5
|
// src/pool.ts
|
|
6
6
|
import pg from "pg";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
function getCacheKey(uri, options) {
|
|
8
|
+
const sortedKeys = Object.keys(options).sort();
|
|
9
|
+
const sorted = {};
|
|
10
|
+
for (const key of sortedKeys) {
|
|
11
|
+
sorted[key] = options[key];
|
|
12
|
+
}
|
|
13
|
+
return `${uri}::${JSON.stringify(sorted)}`;
|
|
14
|
+
}
|
|
15
|
+
var createPoolManager = () => {
|
|
16
|
+
const pools = /* @__PURE__ */ new Map();
|
|
17
|
+
return {
|
|
18
|
+
getPool(uri, options = {}) {
|
|
19
|
+
const key = getCacheKey(uri, options);
|
|
20
|
+
let existingPool = pools.get(key);
|
|
21
|
+
if (!existingPool) {
|
|
22
|
+
existingPool = new pg.Pool({ connectionString: uri, ...options });
|
|
23
|
+
pools.set(key, existingPool);
|
|
24
|
+
}
|
|
25
|
+
return existingPool;
|
|
26
|
+
},
|
|
27
|
+
async endPool(uri, options = {}) {
|
|
28
|
+
const key = getCacheKey(uri, options);
|
|
29
|
+
const existingPool = pools.get(key);
|
|
30
|
+
if (existingPool) {
|
|
31
|
+
await existingPool.end();
|
|
32
|
+
pools.delete(key);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async endAllPools() {
|
|
36
|
+
const endings = [];
|
|
37
|
+
for (const [, p] of pools) {
|
|
38
|
+
endings.push(p.end());
|
|
39
|
+
}
|
|
40
|
+
await Promise.all(endings);
|
|
41
|
+
pools.clear();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
20
44
|
};
|
|
45
|
+
var poolManager = createPoolManager();
|
|
46
|
+
var pool = (uri, options = {}) => poolManager.getPool(uri, options);
|
|
47
|
+
var endPool = async (uri, options = {}) => poolManager.endPool(uri, options);
|
|
21
48
|
|
|
22
49
|
// src/index.ts
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
function escapeIdentifier(identifier) {
|
|
51
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
52
|
+
}
|
|
53
|
+
var KeyvPostgres = class extends Hookified {
|
|
54
|
+
/** Function for executing SQL queries against the PostgreSQL database. */
|
|
26
55
|
query;
|
|
27
|
-
|
|
56
|
+
/** Promise that resolves to the query function once initialization completes. */
|
|
57
|
+
_connected;
|
|
58
|
+
/** The namespace used to prefix keys for multi-tenant separation. */
|
|
59
|
+
_namespace;
|
|
60
|
+
/**
|
|
61
|
+
* The PostgreSQL connection URI.
|
|
62
|
+
* @default 'postgresql://localhost:5432'
|
|
63
|
+
*/
|
|
64
|
+
_uri = "postgresql://localhost:5432";
|
|
65
|
+
/**
|
|
66
|
+
* The table name used for storage.
|
|
67
|
+
* @default 'keyv'
|
|
68
|
+
*/
|
|
69
|
+
_table = "keyv";
|
|
70
|
+
/**
|
|
71
|
+
* The maximum key length (VARCHAR length) for the key column.
|
|
72
|
+
* @default 255
|
|
73
|
+
*/
|
|
74
|
+
_keyLength = 255;
|
|
75
|
+
/**
|
|
76
|
+
* The maximum namespace length (VARCHAR length) for the namespace column.
|
|
77
|
+
* @default 255
|
|
78
|
+
*/
|
|
79
|
+
_namespaceLength = 255;
|
|
80
|
+
/**
|
|
81
|
+
* The PostgreSQL schema name.
|
|
82
|
+
* @default 'public'
|
|
83
|
+
*/
|
|
84
|
+
_schema = "public";
|
|
85
|
+
/**
|
|
86
|
+
* The SSL configuration for the PostgreSQL connection.
|
|
87
|
+
* @default undefined
|
|
88
|
+
*/
|
|
89
|
+
_ssl;
|
|
90
|
+
/**
|
|
91
|
+
* The number of rows to fetch per iteration batch.
|
|
92
|
+
* @default 10
|
|
93
|
+
*/
|
|
94
|
+
_iterationLimit = 10;
|
|
95
|
+
/**
|
|
96
|
+
* Whether to use a PostgreSQL unlogged table (faster writes, no WAL, data lost on crash).
|
|
97
|
+
* @default false
|
|
98
|
+
*/
|
|
99
|
+
_useUnloggedTable = false;
|
|
100
|
+
/**
|
|
101
|
+
* The interval in milliseconds between automatic expired-entry cleanup runs.
|
|
102
|
+
* A value of 0 (default) disables the automatic cleanup.
|
|
103
|
+
* @default 0
|
|
104
|
+
*/
|
|
105
|
+
_clearExpiredInterval = 0;
|
|
106
|
+
/**
|
|
107
|
+
* The timer reference for the automatic expired-entry cleanup interval.
|
|
108
|
+
*/
|
|
109
|
+
_clearExpiredTimer;
|
|
110
|
+
/**
|
|
111
|
+
* Additional PoolConfig properties passed through to the pg connection pool.
|
|
112
|
+
*/
|
|
113
|
+
_poolConfig = {};
|
|
114
|
+
/**
|
|
115
|
+
* Creates a new KeyvPostgres instance.
|
|
116
|
+
* @param options - A PostgreSQL connection URI string or a {@link KeyvPostgresOptions} configuration object.
|
|
117
|
+
*/
|
|
28
118
|
constructor(options) {
|
|
29
119
|
super();
|
|
30
|
-
this.ttlSupport = false;
|
|
31
120
|
if (typeof options === "string") {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
uri
|
|
36
|
-
};
|
|
37
|
-
} else {
|
|
38
|
-
options = {
|
|
39
|
-
dialect: "postgres",
|
|
40
|
-
uri: "postgresql://localhost:5432",
|
|
41
|
-
...options
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
this.opts = {
|
|
45
|
-
table: "keyv",
|
|
46
|
-
schema: "public",
|
|
47
|
-
keySize: 255,
|
|
48
|
-
...options
|
|
49
|
-
};
|
|
50
|
-
let createTable = `CREATE${this.opts.useUnloggedTable ? " UNLOGGED " : " "}TABLE IF NOT EXISTS ${this.opts.schema}.${this.opts.table}(key VARCHAR(${Number(this.opts.keySize)}) PRIMARY KEY, value TEXT )`;
|
|
51
|
-
if (this.opts.schema !== "public") {
|
|
52
|
-
createTable = `CREATE SCHEMA IF NOT EXISTS ${this.opts.schema}; ${createTable}`;
|
|
121
|
+
this._uri = options;
|
|
122
|
+
} else if (options) {
|
|
123
|
+
this.setOptions(options);
|
|
53
124
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
125
|
+
const schemaEsc = escapeIdentifier(this._schema);
|
|
126
|
+
const tableEsc = escapeIdentifier(this._table);
|
|
127
|
+
let createTable = `CREATE${this._useUnloggedTable ? " UNLOGGED " : " "}TABLE IF NOT EXISTS ${schemaEsc}.${tableEsc}(key VARCHAR(${Number(this._keyLength)}) NOT NULL, value TEXT, namespace VARCHAR(${Number(this._namespaceLength)}) DEFAULT NULL, expires BIGINT DEFAULT NULL)`;
|
|
128
|
+
if (this._schema !== "public") {
|
|
129
|
+
createTable = `CREATE SCHEMA IF NOT EXISTS ${schemaEsc}; ${createTable}`;
|
|
130
|
+
}
|
|
131
|
+
const migration = `ALTER TABLE ${schemaEsc}.${tableEsc} ADD COLUMN IF NOT EXISTS namespace VARCHAR(${Number(this._namespaceLength)}) DEFAULT NULL`;
|
|
132
|
+
const migrationExpires = `ALTER TABLE ${schemaEsc}.${tableEsc} ADD COLUMN IF NOT EXISTS expires BIGINT DEFAULT NULL`;
|
|
133
|
+
const dropOldPk = `ALTER TABLE ${schemaEsc}.${tableEsc} DROP CONSTRAINT IF EXISTS ${escapeIdentifier(`${this._table}_pkey`)}`;
|
|
134
|
+
const createIndex = `CREATE UNIQUE INDEX IF NOT EXISTS ${escapeIdentifier(`${this._table}_key_namespace_idx`)} ON ${schemaEsc}.${tableEsc} (key, COALESCE(namespace, ''))`;
|
|
135
|
+
const createExpiresIndex = `CREATE INDEX IF NOT EXISTS ${escapeIdentifier(`${this._table}_expires_idx`)} ON ${schemaEsc}.${tableEsc} (expires) WHERE expires IS NOT NULL`;
|
|
136
|
+
this._connected = this.init(
|
|
137
|
+
createTable,
|
|
138
|
+
migration,
|
|
139
|
+
migrationExpires,
|
|
140
|
+
dropOldPk,
|
|
141
|
+
createIndex,
|
|
142
|
+
createExpiresIndex
|
|
143
|
+
).catch((error) => {
|
|
144
|
+
this.emit("error", error);
|
|
145
|
+
throw error;
|
|
146
|
+
});
|
|
147
|
+
this.query = async (sqlString, values) => {
|
|
148
|
+
const query = await this._connected;
|
|
149
|
+
return query(sqlString, values);
|
|
150
|
+
};
|
|
151
|
+
this.startClearExpiredTimer();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Initializes the database connection and ensures the table schema exists.
|
|
155
|
+
* Called from the constructor; errors are emitted rather than thrown.
|
|
156
|
+
*/
|
|
157
|
+
async init(createTable, migration, migrationExpires, dropOldPk, createIndex, createExpiresIndex) {
|
|
158
|
+
const query = await this.connect();
|
|
159
|
+
try {
|
|
160
|
+
await query(createTable);
|
|
161
|
+
await query(migration);
|
|
162
|
+
await query(migrationExpires);
|
|
163
|
+
await query(dropOldPk);
|
|
164
|
+
await query(createIndex);
|
|
165
|
+
await query(createExpiresIndex);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error.code !== "23505") {
|
|
168
|
+
this.emit("error", error);
|
|
62
169
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
170
|
+
}
|
|
171
|
+
return query;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get the namespace for the adapter. If undefined, no namespace prefix is applied.
|
|
175
|
+
*/
|
|
176
|
+
get namespace() {
|
|
177
|
+
return this._namespace;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Set the namespace for the adapter. Used for key prefixing and scoping operations like `clear()`.
|
|
181
|
+
*/
|
|
182
|
+
set namespace(value) {
|
|
183
|
+
this._namespace = value;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get the PostgreSQL connection URI.
|
|
187
|
+
* @default 'postgresql://localhost:5432'
|
|
188
|
+
*/
|
|
189
|
+
get uri() {
|
|
190
|
+
return this._uri;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Set the PostgreSQL connection URI.
|
|
194
|
+
*/
|
|
195
|
+
set uri(value) {
|
|
196
|
+
this._uri = value;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get the table name used for storage.
|
|
200
|
+
* @default 'keyv'
|
|
201
|
+
*/
|
|
202
|
+
get table() {
|
|
203
|
+
return this._table;
|
|
66
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Set the table name used for storage.
|
|
207
|
+
*/
|
|
208
|
+
set table(value) {
|
|
209
|
+
this._table = value;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get the maximum key length (VARCHAR length) for the key column.
|
|
213
|
+
* @default 255
|
|
214
|
+
*/
|
|
215
|
+
get keyLength() {
|
|
216
|
+
return this._keyLength;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Set the maximum key length (VARCHAR length) for the key column.
|
|
220
|
+
*/
|
|
221
|
+
set keyLength(value) {
|
|
222
|
+
this._keyLength = value;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get the maximum namespace length (VARCHAR length) for the namespace column.
|
|
226
|
+
* @default 255
|
|
227
|
+
*/
|
|
228
|
+
get namespaceLength() {
|
|
229
|
+
return this._namespaceLength;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Set the maximum namespace length (VARCHAR length) for the namespace column.
|
|
233
|
+
*/
|
|
234
|
+
set namespaceLength(value) {
|
|
235
|
+
this._namespaceLength = value;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get the PostgreSQL schema name.
|
|
239
|
+
* @default 'public'
|
|
240
|
+
*/
|
|
241
|
+
get schema() {
|
|
242
|
+
return this._schema;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Set the PostgreSQL schema name.
|
|
246
|
+
*/
|
|
247
|
+
set schema(value) {
|
|
248
|
+
this._schema = value;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get the SSL configuration for the PostgreSQL connection.
|
|
252
|
+
* @default undefined
|
|
253
|
+
*/
|
|
254
|
+
get ssl() {
|
|
255
|
+
return this._ssl;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Set the SSL configuration for the PostgreSQL connection.
|
|
259
|
+
*/
|
|
260
|
+
set ssl(value) {
|
|
261
|
+
this._ssl = value;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the number of rows to fetch per iteration batch.
|
|
265
|
+
* @default 10
|
|
266
|
+
*/
|
|
267
|
+
get iterationLimit() {
|
|
268
|
+
return this._iterationLimit;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Set the number of rows to fetch per iteration batch.
|
|
272
|
+
*/
|
|
273
|
+
set iterationLimit(value) {
|
|
274
|
+
this._iterationLimit = value;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get whether to use a PostgreSQL unlogged table (faster writes, no WAL, data lost on crash).
|
|
278
|
+
* @default false
|
|
279
|
+
*/
|
|
280
|
+
get useUnloggedTable() {
|
|
281
|
+
return this._useUnloggedTable;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Set whether to use a PostgreSQL unlogged table.
|
|
285
|
+
*/
|
|
286
|
+
set useUnloggedTable(value) {
|
|
287
|
+
this._useUnloggedTable = value;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get the interval in milliseconds between automatic expired-entry cleanup runs.
|
|
291
|
+
* A value of 0 means the automatic cleanup is disabled.
|
|
292
|
+
* @default 0
|
|
293
|
+
*/
|
|
294
|
+
get clearExpiredInterval() {
|
|
295
|
+
return this._clearExpiredInterval;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Set the interval in milliseconds between automatic expired-entry cleanup runs.
|
|
299
|
+
* Setting to 0 disables the automatic cleanup.
|
|
300
|
+
*/
|
|
301
|
+
set clearExpiredInterval(value) {
|
|
302
|
+
this._clearExpiredInterval = value;
|
|
303
|
+
this.startClearExpiredTimer();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get the options for the adapter. This is required by the KeyvStoreAdapter interface.
|
|
307
|
+
*/
|
|
308
|
+
// biome-ignore lint/suspicious/noExplicitAny: type format
|
|
309
|
+
get opts() {
|
|
310
|
+
return {
|
|
311
|
+
uri: this._uri,
|
|
312
|
+
table: this._table,
|
|
313
|
+
keyLength: this._keyLength,
|
|
314
|
+
namespaceLength: this._namespaceLength,
|
|
315
|
+
schema: this._schema,
|
|
316
|
+
ssl: this._ssl,
|
|
317
|
+
dialect: "postgres",
|
|
318
|
+
iterationLimit: this._iterationLimit,
|
|
319
|
+
useUnloggedTable: this._useUnloggedTable,
|
|
320
|
+
clearExpiredInterval: this._clearExpiredInterval,
|
|
321
|
+
...this._poolConfig
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Set the options for the adapter.
|
|
326
|
+
*/
|
|
327
|
+
set opts(options) {
|
|
328
|
+
this.setOptions(options);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Gets a value by key.
|
|
332
|
+
* @param key - The key to retrieve.
|
|
333
|
+
* @returns The value associated with the key, or `undefined` if not found.
|
|
334
|
+
*/
|
|
67
335
|
async get(key) {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
336
|
+
const strippedKey = this.removeKeyPrefix(key);
|
|
337
|
+
const select = `SELECT * FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE key = $1 AND COALESCE(namespace, '') = COALESCE($2, '')`;
|
|
338
|
+
const rows = await this.query(select, [
|
|
339
|
+
strippedKey,
|
|
340
|
+
this.getNamespaceValue()
|
|
341
|
+
]);
|
|
70
342
|
const row = rows[0];
|
|
71
343
|
return row === void 0 ? void 0 : row.value;
|
|
72
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Gets multiple values by their keys.
|
|
347
|
+
* @param keys - An array of keys to retrieve.
|
|
348
|
+
* @returns An array of values in the same order as the keys, with `undefined` for missing keys.
|
|
349
|
+
*/
|
|
73
350
|
async getMany(keys) {
|
|
74
|
-
const
|
|
75
|
-
const
|
|
351
|
+
const strippedKeys = keys.map((k) => this.removeKeyPrefix(k));
|
|
352
|
+
const getMany = `SELECT * FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE key = ANY($1) AND COALESCE(namespace, '') = COALESCE($2, '')`;
|
|
353
|
+
const rows = await this.query(getMany, [
|
|
354
|
+
strippedKeys,
|
|
355
|
+
this.getNamespaceValue()
|
|
356
|
+
]);
|
|
76
357
|
const rowsMap = new Map(rows.map((row) => [row.key, row]));
|
|
77
|
-
return
|
|
358
|
+
return strippedKeys.map((key) => rowsMap.get(key)?.value);
|
|
78
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Sets a key-value pair. Uses an upsert operation via `ON CONFLICT` to insert or update.
|
|
362
|
+
* @param key - The key to set.
|
|
363
|
+
* @param value - The value to store.
|
|
364
|
+
*/
|
|
79
365
|
// biome-ignore lint/suspicious/noExplicitAny: type format
|
|
80
366
|
async set(key, value) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
367
|
+
const strippedKey = this.removeKeyPrefix(key);
|
|
368
|
+
const expires = this.getExpiresFromValue(value);
|
|
369
|
+
const upsert = `INSERT INTO ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} (key, value, namespace, expires)
|
|
370
|
+
VALUES($1, $2, $3, $4)
|
|
371
|
+
ON CONFLICT(key, COALESCE(namespace, ''))
|
|
372
|
+
DO UPDATE SET value=excluded.value, expires=excluded.expires;`;
|
|
373
|
+
await this.query(upsert, [
|
|
374
|
+
strippedKey,
|
|
375
|
+
value,
|
|
376
|
+
this.getNamespaceValue(),
|
|
377
|
+
expires
|
|
378
|
+
]);
|
|
86
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Sets multiple key-value pairs at once using PostgreSQL `UNNEST` for efficient bulk operations.
|
|
382
|
+
* @param entries - An array of key-value entry objects.
|
|
383
|
+
*/
|
|
87
384
|
async setMany(entries) {
|
|
88
385
|
const keys = [];
|
|
89
386
|
const values = [];
|
|
387
|
+
const expiresArray = [];
|
|
90
388
|
for (const { key, value } of entries) {
|
|
91
|
-
keys.push(key);
|
|
389
|
+
keys.push(this.removeKeyPrefix(key));
|
|
92
390
|
values.push(value);
|
|
391
|
+
expiresArray.push(this.getExpiresFromValue(value));
|
|
93
392
|
}
|
|
94
|
-
const upsert = `INSERT INTO ${this.
|
|
95
|
-
SELECT
|
|
96
|
-
ON CONFLICT(key)
|
|
97
|
-
DO UPDATE SET value=excluded.value;`;
|
|
98
|
-
await this.query(upsert, [
|
|
393
|
+
const upsert = `INSERT INTO ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} (key, value, namespace, expires)
|
|
394
|
+
SELECT k, v, $3, e FROM UNNEST($1::text[], $2::text[], $4::bigint[]) AS t(k, v, e)
|
|
395
|
+
ON CONFLICT(key, COALESCE(namespace, ''))
|
|
396
|
+
DO UPDATE SET value=excluded.value, expires=excluded.expires;`;
|
|
397
|
+
await this.query(upsert, [
|
|
398
|
+
keys,
|
|
399
|
+
values,
|
|
400
|
+
this.getNamespaceValue(),
|
|
401
|
+
expiresArray
|
|
402
|
+
]);
|
|
99
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Deletes a key from the store.
|
|
406
|
+
* @param key - The key to delete.
|
|
407
|
+
* @returns `true` if the key existed and was deleted, `false` otherwise.
|
|
408
|
+
*/
|
|
100
409
|
async delete(key) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
await this.query(del, [key]);
|
|
108
|
-
return true;
|
|
410
|
+
const strippedKey = this.removeKeyPrefix(key);
|
|
411
|
+
const ns = this.getNamespaceValue();
|
|
412
|
+
const del = `DELETE FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE key = $1 AND COALESCE(namespace, '') = COALESCE($2, '') RETURNING 1`;
|
|
413
|
+
const rows = await this.query(del, [strippedKey, ns]);
|
|
414
|
+
return rows.length > 0;
|
|
109
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Deletes multiple keys from the store at once.
|
|
418
|
+
* @param keys - An array of keys to delete.
|
|
419
|
+
* @returns `true` if any of the keys existed and were deleted, `false` otherwise.
|
|
420
|
+
*/
|
|
110
421
|
async deleteMany(keys) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
await this.query(del, [keys]);
|
|
118
|
-
return true;
|
|
422
|
+
const strippedKeys = keys.map((k) => this.removeKeyPrefix(k));
|
|
423
|
+
const ns = this.getNamespaceValue();
|
|
424
|
+
const del = `DELETE FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE key = ANY($1) AND COALESCE(namespace, '') = COALESCE($2, '') RETURNING 1`;
|
|
425
|
+
const rows = await this.query(del, [strippedKeys, ns]);
|
|
426
|
+
return rows.length > 0;
|
|
119
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Clears all keys in the current namespace. If no namespace is set, all keys are removed.
|
|
430
|
+
*/
|
|
120
431
|
async clear() {
|
|
121
|
-
|
|
122
|
-
|
|
432
|
+
if (this._namespace) {
|
|
433
|
+
const del = `DELETE FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE namespace = $1`;
|
|
434
|
+
await this.query(del, [this._namespace]);
|
|
435
|
+
} else {
|
|
436
|
+
const del = `DELETE FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE namespace IS NULL`;
|
|
437
|
+
await this.query(del);
|
|
438
|
+
}
|
|
123
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Utility helper method to delete all expired entries from the store where the `expires` column is less than the current timestamp.
|
|
442
|
+
*/
|
|
443
|
+
async clearExpired() {
|
|
444
|
+
const del = `DELETE FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE expires IS NOT NULL AND expires < $1`;
|
|
445
|
+
await this.query(del, [Date.now()]);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Iterates over all key-value pairs, optionally filtered by namespace.
|
|
449
|
+
* Uses cursor-based (keyset) pagination with batch size controlled by `iterationLimit`.
|
|
450
|
+
* @param namespace - Optional namespace to filter keys by.
|
|
451
|
+
* @yields A `[key, value]` tuple for each entry.
|
|
452
|
+
*/
|
|
124
453
|
async *iterator(namespace) {
|
|
125
|
-
const limit = Number.parseInt(String(this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
454
|
+
const limit = Number.parseInt(String(this._iterationLimit), 10) || 10;
|
|
455
|
+
const namespaceValue = namespace ?? null;
|
|
456
|
+
let lastKey = null;
|
|
457
|
+
while (true) {
|
|
458
|
+
let entries;
|
|
459
|
+
try {
|
|
460
|
+
const where = [];
|
|
461
|
+
const params = [];
|
|
462
|
+
if (namespaceValue !== null) {
|
|
463
|
+
where.push(`namespace = $${params.length + 1}`);
|
|
464
|
+
params.push(namespaceValue);
|
|
465
|
+
} else {
|
|
466
|
+
where.push("namespace IS NULL");
|
|
467
|
+
}
|
|
468
|
+
if (lastKey !== null) {
|
|
469
|
+
where.push(`key > $${params.length + 1}`);
|
|
470
|
+
params.push(lastKey);
|
|
471
|
+
}
|
|
472
|
+
const select = `SELECT * FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE ${where.join(" AND ")} ORDER BY key LIMIT $${params.length + 1}`;
|
|
473
|
+
params.push(limit);
|
|
474
|
+
entries = await this.query(select, params);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
this.emit(
|
|
477
|
+
"error",
|
|
478
|
+
new Error(
|
|
479
|
+
`Iterator failed at cursor ${lastKey ?? "start"}: ${error.message}`
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
134
484
|
if (entries.length === 0) {
|
|
135
485
|
return;
|
|
136
486
|
}
|
|
137
487
|
for (const entry of entries) {
|
|
138
|
-
|
|
139
|
-
|
|
488
|
+
if (entry.key !== void 0 && entry.key !== null) {
|
|
489
|
+
const prefixedKey = namespace ? `${namespace}:${entry.key}` : entry.key;
|
|
490
|
+
yield [prefixedKey, entry.value];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
lastKey = entries[entries.length - 1].key;
|
|
494
|
+
if (entries.length < limit) {
|
|
495
|
+
return;
|
|
140
496
|
}
|
|
141
|
-
yield* iterate(offset, options, query);
|
|
142
497
|
}
|
|
143
|
-
yield* iterate(0, this.opts, this.query);
|
|
144
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Checks whether a key exists in the store.
|
|
501
|
+
* @param key - The key to check.
|
|
502
|
+
* @returns `true` if the key exists, `false` otherwise.
|
|
503
|
+
*/
|
|
145
504
|
async has(key) {
|
|
146
|
-
const
|
|
147
|
-
const
|
|
505
|
+
const strippedKey = this.removeKeyPrefix(key);
|
|
506
|
+
const exists = `SELECT EXISTS ( SELECT * FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE key = $1 AND COALESCE(namespace, '') = COALESCE($2, '') )`;
|
|
507
|
+
const rows = await this.query(exists, [
|
|
508
|
+
strippedKey,
|
|
509
|
+
this.getNamespaceValue()
|
|
510
|
+
]);
|
|
148
511
|
return rows[0].exists;
|
|
149
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Checks whether multiple keys exist in the store.
|
|
515
|
+
* @param keys - An array of keys to check.
|
|
516
|
+
* @returns An array of booleans in the same order as the input keys.
|
|
517
|
+
*/
|
|
518
|
+
async hasMany(keys) {
|
|
519
|
+
const strippedKeys = keys.map((k) => this.removeKeyPrefix(k));
|
|
520
|
+
const select = `SELECT key FROM ${escapeIdentifier(this._schema)}.${escapeIdentifier(this._table)} WHERE key = ANY($1) AND COALESCE(namespace, '') = COALESCE($2, '')`;
|
|
521
|
+
const rows = await this.query(select, [
|
|
522
|
+
strippedKeys,
|
|
523
|
+
this.getNamespaceValue()
|
|
524
|
+
]);
|
|
525
|
+
const existingKeys = new Set(rows.map((row) => row.key));
|
|
526
|
+
return strippedKeys.map((key) => existingKeys.has(key));
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Establishes a connection to the PostgreSQL database via the connection pool.
|
|
530
|
+
* @returns A query function that executes SQL statements and returns result rows.
|
|
531
|
+
*/
|
|
150
532
|
async connect() {
|
|
151
|
-
const conn = pool(this.
|
|
533
|
+
const conn = pool(this._uri, { ...this._poolConfig, ssl: this._ssl });
|
|
152
534
|
return async (sql, values) => {
|
|
153
535
|
const data = await conn.query(sql, values);
|
|
154
536
|
return data.rows;
|
|
155
537
|
};
|
|
156
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Disconnects from the PostgreSQL database and releases the connection pool.
|
|
541
|
+
* Also stops the automatic expired-entry cleanup interval if running.
|
|
542
|
+
*/
|
|
157
543
|
async disconnect() {
|
|
158
|
-
|
|
544
|
+
this.stopClearExpiredTimer();
|
|
545
|
+
await endPool(this._uri, { ...this._poolConfig, ssl: this._ssl });
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Strips the namespace prefix from a key that was added by the Keyv core.
|
|
549
|
+
* For example, if namespace is "ns" and key is "ns:foo", returns "foo".
|
|
550
|
+
*/
|
|
551
|
+
removeKeyPrefix(key) {
|
|
552
|
+
if (this._namespace && key.startsWith(`${this._namespace}:`)) {
|
|
553
|
+
return key.slice(this._namespace.length + 1);
|
|
554
|
+
}
|
|
555
|
+
return key;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Returns the namespace value for SQL parameters. Returns null when no namespace is set.
|
|
559
|
+
*/
|
|
560
|
+
getNamespaceValue() {
|
|
561
|
+
return this._namespace ?? null;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Extracts the `expires` timestamp from a serialized value.
|
|
565
|
+
* The Keyv core serializes data as JSON like `{"value":"...","expires":1234567890}`.
|
|
566
|
+
* Returns the expires value as a number, or null if not present or not parseable.
|
|
567
|
+
*/
|
|
568
|
+
// biome-ignore lint/suspicious/noExplicitAny: type format
|
|
569
|
+
getExpiresFromValue(value) {
|
|
570
|
+
let data;
|
|
571
|
+
if (typeof value === "string") {
|
|
572
|
+
try {
|
|
573
|
+
data = JSON.parse(value);
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
data = value;
|
|
579
|
+
}
|
|
580
|
+
if (data && typeof data === "object" && typeof data.expires === "number") {
|
|
581
|
+
return data.expires;
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Starts (or restarts) the automatic expired-entry cleanup interval.
|
|
587
|
+
* If the interval is 0 or negative, any existing timer is stopped.
|
|
588
|
+
*/
|
|
589
|
+
startClearExpiredTimer() {
|
|
590
|
+
this.stopClearExpiredTimer();
|
|
591
|
+
if (this._clearExpiredInterval > 0) {
|
|
592
|
+
this._clearExpiredTimer = setInterval(async () => {
|
|
593
|
+
try {
|
|
594
|
+
await this.clearExpired();
|
|
595
|
+
} catch (error) {
|
|
596
|
+
this.emit("error", error);
|
|
597
|
+
}
|
|
598
|
+
}, this._clearExpiredInterval);
|
|
599
|
+
this._clearExpiredTimer.unref();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Stops the automatic expired-entry cleanup interval if running.
|
|
604
|
+
*/
|
|
605
|
+
stopClearExpiredTimer() {
|
|
606
|
+
if (this._clearExpiredTimer) {
|
|
607
|
+
clearInterval(this._clearExpiredTimer);
|
|
608
|
+
this._clearExpiredTimer = void 0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
setOptions(options) {
|
|
612
|
+
if (options.uri !== void 0) {
|
|
613
|
+
this._uri = options.uri;
|
|
614
|
+
}
|
|
615
|
+
if (options.table !== void 0) {
|
|
616
|
+
this._table = options.table;
|
|
617
|
+
}
|
|
618
|
+
if (options.keyLength !== void 0) {
|
|
619
|
+
this._keyLength = options.keyLength;
|
|
620
|
+
}
|
|
621
|
+
if (options.namespaceLength !== void 0) {
|
|
622
|
+
this._namespaceLength = options.namespaceLength;
|
|
623
|
+
}
|
|
624
|
+
if (options.schema !== void 0) {
|
|
625
|
+
this._schema = options.schema;
|
|
626
|
+
}
|
|
627
|
+
if (options.ssl !== void 0) {
|
|
628
|
+
this._ssl = options.ssl;
|
|
629
|
+
}
|
|
630
|
+
if (options.iterationLimit !== void 0) {
|
|
631
|
+
this._iterationLimit = options.iterationLimit;
|
|
632
|
+
}
|
|
633
|
+
if (options.useUnloggedTable !== void 0) {
|
|
634
|
+
this._useUnloggedTable = options.useUnloggedTable;
|
|
635
|
+
}
|
|
636
|
+
if (options.clearExpiredInterval !== void 0) {
|
|
637
|
+
this._clearExpiredInterval = options.clearExpiredInterval;
|
|
638
|
+
}
|
|
639
|
+
const {
|
|
640
|
+
uri,
|
|
641
|
+
table,
|
|
642
|
+
keyLength,
|
|
643
|
+
namespaceLength,
|
|
644
|
+
schema,
|
|
645
|
+
ssl,
|
|
646
|
+
iterationLimit,
|
|
647
|
+
useUnloggedTable,
|
|
648
|
+
clearExpiredInterval,
|
|
649
|
+
...poolConfigRest
|
|
650
|
+
} = options;
|
|
651
|
+
this._poolConfig = { ...this._poolConfig, ...poolConfigRest };
|
|
159
652
|
}
|
|
160
653
|
};
|
|
161
654
|
var createKeyv = (options) => new Keyv({ store: new KeyvPostgres(options) });
|
|
@@ -166,3 +659,4 @@ export {
|
|
|
166
659
|
index_default as default
|
|
167
660
|
};
|
|
168
661
|
/* v8 ignore next -- @preserve */
|
|
662
|
+
/* v8 ignore start -- @preserve */
|