@fedify/sqlite 2.0.0-dev.1908 → 2.0.0-dev.206

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright 2024–2025 Hong Minhee
3
+ Copyright 2024–2026 Hong Minhee
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
package/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  This package provides a SQLite-based [`KvStore`] implementation for [Fedify].
8
8
 
9
- [JSR]: https://jsr.io/@fedify/sqlite
10
9
  [JSR badge]: https://jsr.io/badges/@fedify/sqlite
11
- [npm]: https://www.npmjs.com/package/@fedify/sqlite
10
+ [JSR]: https://jsr.io/@fedify/sqlite
12
11
  [npm badge]: https://img.shields.io/npm/v/@fedify/sqlite?logo=npm
13
- [Fedify]: https://fedify.dev/
12
+ [npm]: https://www.npmjs.com/package/@fedify/sqlite
14
13
  [`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore
14
+ [Fedify]: https://fedify.dev/
15
15
 
16
16
 
17
17
  Usage
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/sqlite",
3
- "version": "2.0.0-dev.1908+c31cc639",
3
+ "version": "2.0.0-dev.206+fa815928",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
@@ -16,6 +16,7 @@
16
16
  ],
17
17
  "publish": {
18
18
  "exclude": [
19
+ "**/*.test.ts",
19
20
  "!dist/"
20
21
  ]
21
22
  },
package/dist/kv.cjs CHANGED
@@ -134,6 +134,36 @@ var SqliteKvStore = class SqliteKvStore {
134
134
  }
135
135
  }
136
136
  /**
137
+ * {@inheritDoc KvStore.list}
138
+ * @since 1.10.0
139
+ */
140
+ async *list(prefix) {
141
+ this.initialize();
142
+ const now = Temporal.Now.instant().epochMilliseconds;
143
+ let results;
144
+ if (prefix == null || prefix.length === 0) results = this.#db.prepare(`
145
+ SELECT key, value
146
+ FROM "${this.#tableName}"
147
+ WHERE expires IS NULL OR expires > ?
148
+ ORDER BY key
149
+ `).all(now);
150
+ else {
151
+ const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
152
+ const exactKey = JSON.stringify(prefix);
153
+ results = this.#db.prepare(`
154
+ SELECT key, value
155
+ FROM "${this.#tableName}"
156
+ WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
157
+ AND (expires IS NULL OR expires > ?)
158
+ ORDER BY key
159
+ `).all(pattern, exactKey, now);
160
+ }
161
+ for (const row of results) yield {
162
+ key: this.#decodeKey(row.key),
163
+ value: this.#decodeValue(row.value)
164
+ };
165
+ }
166
+ /**
137
167
  * Creates the table used by the key–value store if it does not already exist.
138
168
  * Does nothing if the table already exists.
139
169
  */
package/dist/kv.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync } from "./dist/sqlite.node.cjs";
2
- import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
2
+ import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
3
3
 
4
4
  //#region src/kv.d.ts
5
5
  /**
@@ -12,12 +12,12 @@ interface SqliteKvStoreOptions {
12
12
  * `"fedify_kv"` by default.
13
13
  * @default `"fedify_kv"`
14
14
  */
15
- tableName?: string;
15
+ readonly tableName?: string;
16
16
  /**
17
17
  * Whether the table has been initialized. `false` by default.
18
18
  * @default `false`
19
19
  */
20
- initialized?: boolean;
20
+ readonly initialized?: boolean;
21
21
  }
22
22
  /**
23
23
  * A key–value store that uses SQLite as the underlying storage.
@@ -61,6 +61,11 @@ declare class SqliteKvStore implements KvStore {
61
61
  * {@inheritDoc KvStore.cas}
62
62
  */
63
63
  cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
64
+ /**
65
+ * {@inheritDoc KvStore.list}
66
+ * @since 1.10.0
67
+ */
68
+ list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
64
69
  /**
65
70
  * Creates the table used by the key–value store if it does not already exist.
66
71
  * Does nothing if the table already exists.
package/dist/kv.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { PlatformDatabase } from "#sqlite";
3
- import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
3
+ import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
4
4
 
5
5
  //#region src/kv.d.ts
6
6
  /**
@@ -13,12 +13,12 @@ interface SqliteKvStoreOptions {
13
13
  * `"fedify_kv"` by default.
14
14
  * @default `"fedify_kv"`
15
15
  */
16
- tableName?: string;
16
+ readonly tableName?: string;
17
17
  /**
18
18
  * Whether the table has been initialized. `false` by default.
19
19
  * @default `false`
20
20
  */
21
- initialized?: boolean;
21
+ readonly initialized?: boolean;
22
22
  }
23
23
  /**
24
24
  * A key–value store that uses SQLite as the underlying storage.
@@ -62,6 +62,11 @@ declare class SqliteKvStore implements KvStore {
62
62
  * {@inheritDoc KvStore.cas}
63
63
  */
64
64
  cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
65
+ /**
66
+ * {@inheritDoc KvStore.list}
67
+ * @since 1.10.0
68
+ */
69
+ list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
65
70
  /**
66
71
  * Creates the table used by the key–value store if it does not already exist.
67
72
  * Does nothing if the table already exists.
package/dist/kv.js CHANGED
@@ -133,6 +133,36 @@ var SqliteKvStore = class SqliteKvStore {
133
133
  }
134
134
  }
135
135
  /**
136
+ * {@inheritDoc KvStore.list}
137
+ * @since 1.10.0
138
+ */
139
+ async *list(prefix) {
140
+ this.initialize();
141
+ const now = Temporal.Now.instant().epochMilliseconds;
142
+ let results;
143
+ if (prefix == null || prefix.length === 0) results = this.#db.prepare(`
144
+ SELECT key, value
145
+ FROM "${this.#tableName}"
146
+ WHERE expires IS NULL OR expires > ?
147
+ ORDER BY key
148
+ `).all(now);
149
+ else {
150
+ const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
151
+ const exactKey = JSON.stringify(prefix);
152
+ results = this.#db.prepare(`
153
+ SELECT key, value
154
+ FROM "${this.#tableName}"
155
+ WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
156
+ AND (expires IS NULL OR expires > ?)
157
+ ORDER BY key
158
+ `).all(pattern, exactKey, now);
159
+ }
160
+ for (const row of results) yield {
161
+ key: this.#decodeKey(row.key),
162
+ value: this.#decodeValue(row.value)
163
+ };
164
+ }
165
+ /**
136
166
  * Creates the table used by the key–value store if it does not already exist.
137
167
  * Does nothing if the table already exists.
138
168
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/sqlite",
3
- "version": "2.0.0-dev.1908+c31cc639",
3
+ "version": "2.0.0-dev.206+fa815928",
4
4
  "description": "SQLite drivers for Fedify",
5
5
  "keywords": [
6
6
  "fedify",
@@ -58,11 +58,11 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@js-temporal/polyfill": "^0.5.1",
61
- "@logtape/logtape": "^1.1.1",
61
+ "@logtape/logtape": "^2.0.0",
62
62
  "es-toolkit": "^1.31.0"
63
63
  },
64
64
  "peerDependencies": {
65
- "@fedify/fedify": "^2.0.0-dev.1908+c31cc639"
65
+ "@fedify/fedify": "^2.0.0-dev.206+fa815928"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@std/async": "npm:@jsr/std__async@^1.0.13",
package/src/kv.test.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { PlatformDatabase } from "#sqlite";
2
+ import { SqliteKvStore } from "@fedify/sqlite/kv";
2
3
  import * as temporal from "@js-temporal/polyfill";
3
4
  import { delay } from "@std/async/delay";
4
5
  import assert from "node:assert/strict";
5
6
  import { test } from "node:test";
6
- import { SqliteKvStore } from "@fedify/sqlite/kv";
7
7
 
8
8
  let Temporal: typeof temporal.Temporal;
9
9
  if ("Temporal" in globalThis) {
@@ -310,3 +310,96 @@ test("SqliteKvStore.set() - preserves created timestamp on update", async () =>
310
310
  await db.close();
311
311
  }
312
312
  });
313
+
314
+ test("SqliteKvStore.list()", async () => {
315
+ const { db, store } = getStore();
316
+ try {
317
+ await store.set(["prefix", "a"], "value-a");
318
+ await store.set(["prefix", "b"], "value-b");
319
+ await store.set(["prefix", "nested", "c"], "value-c");
320
+ await store.set(["other", "x"], "value-x");
321
+
322
+ const entries: { key: readonly string[]; value: unknown }[] = [];
323
+ for await (const entry of store.list(["prefix"])) {
324
+ entries.push({ key: entry.key, value: entry.value });
325
+ }
326
+
327
+ assert.strictEqual(entries.length, 3);
328
+ assert(
329
+ entries.some((e) => e.key[1] === "a" && e.value === "value-a"),
330
+ );
331
+ assert(entries.some((e) => e.key[1] === "b"));
332
+ assert(entries.some((e) => e.key[1] === "nested"));
333
+ } finally {
334
+ await store.drop();
335
+ await db.close();
336
+ }
337
+ });
338
+
339
+ test("SqliteKvStore.list() - excludes expired", async () => {
340
+ const { db, tableName, store } = getStore();
341
+ try {
342
+ await store.initialize();
343
+ const now = Temporal.Now.instant().epochMilliseconds;
344
+
345
+ // Insert expired entry directly
346
+ db.prepare(`
347
+ INSERT INTO "${tableName}" (key, value, created, expires)
348
+ VALUES (?, ?, ?, ?)
349
+ `).run(
350
+ JSON.stringify(["list-test", "expired"]),
351
+ JSON.stringify("expired-value"),
352
+ now - 1000,
353
+ now - 500,
354
+ );
355
+ await store.set(["list-test", "valid"], "valid-value");
356
+
357
+ const entries: { key: readonly string[]; value: unknown }[] = [];
358
+ for await (const entry of store.list(["list-test"])) {
359
+ entries.push({ key: entry.key, value: entry.value });
360
+ }
361
+
362
+ assert.strictEqual(entries.length, 1);
363
+ assert.deepStrictEqual(entries[0].key, ["list-test", "valid"]);
364
+ } finally {
365
+ await store.drop();
366
+ await db.close();
367
+ }
368
+ });
369
+
370
+ test("SqliteKvStore.list() - single element key", async () => {
371
+ const { db, store } = getStore();
372
+ try {
373
+ await store.set(["a"], "value-a");
374
+ await store.set(["b"], "value-b");
375
+
376
+ const entries: { key: readonly string[]; value: unknown }[] = [];
377
+ for await (const entry of store.list(["a"])) {
378
+ entries.push({ key: entry.key, value: entry.value });
379
+ }
380
+
381
+ assert.strictEqual(entries.length, 1);
382
+ } finally {
383
+ await store.drop();
384
+ await db.close();
385
+ }
386
+ });
387
+
388
+ test("SqliteKvStore.list() - empty prefix", async () => {
389
+ const { db, store } = getStore();
390
+ try {
391
+ await store.set(["a"], "value-a");
392
+ await store.set(["b", "c"], "value-bc");
393
+ await store.set(["d", "e", "f"], "value-def");
394
+
395
+ const entries: { key: readonly string[]; value: unknown }[] = [];
396
+ for await (const entry of store.list()) {
397
+ entries.push({ key: entry.key, value: entry.value });
398
+ }
399
+
400
+ assert.strictEqual(entries.length, 3);
401
+ } finally {
402
+ await store.drop();
403
+ await db.close();
404
+ }
405
+ });
package/src/kv.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { type PlatformDatabase, SqliteDatabase } from "#sqlite";
2
- import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
2
+ import type {
3
+ KvKey,
4
+ KvStore,
5
+ KvStoreListEntry,
6
+ KvStoreSetOptions,
7
+ } from "@fedify/fedify";
3
8
  import { getLogger } from "@logtape/logtape";
4
9
  import { isEqual } from "es-toolkit";
5
10
  import type { SqliteDatabaseAdapter } from "./adapter.ts";
@@ -16,13 +21,13 @@ export interface SqliteKvStoreOptions {
16
21
  * `"fedify_kv"` by default.
17
22
  * @default `"fedify_kv"`
18
23
  */
19
- tableName?: string;
24
+ readonly tableName?: string;
20
25
 
21
26
  /**
22
27
  * Whether the table has been initialized. `false` by default.
23
28
  * @default `false`
24
29
  */
25
- initialized?: boolean;
30
+ readonly initialized?: boolean;
26
31
  }
27
32
 
28
33
  /**
@@ -212,6 +217,51 @@ export class SqliteKvStore implements KvStore {
212
217
  }
213
218
  }
214
219
 
220
+ /**
221
+ * {@inheritDoc KvStore.list}
222
+ * @since 1.10.0
223
+ */
224
+ async *list(prefix?: KvKey): AsyncIterable<KvStoreListEntry> {
225
+ this.initialize();
226
+
227
+ const now = Temporal.Now.instant().epochMilliseconds;
228
+
229
+ let results: { key: string; value: string }[];
230
+
231
+ if (prefix == null || prefix.length === 0) {
232
+ // Empty prefix: return all entries
233
+ results = this.#db
234
+ .prepare(`
235
+ SELECT key, value
236
+ FROM "${this.#tableName}"
237
+ WHERE expires IS NULL OR expires > ?
238
+ ORDER BY key
239
+ `)
240
+ .all(now) as { key: string; value: string }[];
241
+ } else {
242
+ // JSON pattern: '["prefix","' matches keys starting with prefix
243
+ const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
244
+ const exactKey = JSON.stringify(prefix);
245
+
246
+ results = this.#db
247
+ .prepare(`
248
+ SELECT key, value
249
+ FROM "${this.#tableName}"
250
+ WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
251
+ AND (expires IS NULL OR expires > ?)
252
+ ORDER BY key
253
+ `)
254
+ .all(pattern, exactKey, now) as { key: string; value: string }[];
255
+ }
256
+
257
+ for (const row of results) {
258
+ yield {
259
+ key: this.#decodeKey(row.key),
260
+ value: this.#decodeValue(row.value),
261
+ };
262
+ }
263
+ }
264
+
215
265
  /**
216
266
  * Creates the table used by the key–value store if it does not already exist.
217
267
  * Does nothing if the table already exists.