|
| 1 | +# node-sqlite-map |
| 2 | + |
| 3 | +A persistent `Map`-compatible API backed by SQLite using Node.js's built-in `node:sqlite` module. No external database dependencies — just a file (or `:memory:`). |
| 4 | + |
| 5 | +## Requirements |
| 6 | + |
| 7 | +- Node.js >= 22.5.0 (for `node:sqlite`) |
| 8 | + |
| 9 | +## Installation |
| 10 | + |
| 11 | +```bash |
| 12 | +npm install node-sqlite-map |
| 13 | +# or |
| 14 | +pnpm add node-sqlite-map |
| 15 | +``` |
| 16 | + |
| 17 | +## Quick Start |
| 18 | + |
| 19 | +```typescript |
| 20 | +import { SqliteMap } from "node-sqlite-map" |
| 21 | + |
| 22 | +const map = new SqliteMap("./data.db") |
| 23 | + |
| 24 | +map.set("user:1", { name: "Kyle", role: "admin" }) |
| 25 | +map.get("user:1") // { name: "Kyle", role: "admin" } |
| 26 | +map.has("user:1") // true |
| 27 | +map.size // 1 |
| 28 | +``` |
| 29 | + |
| 30 | +Use `:memory:` for an in-memory database that does not persist: |
| 31 | + |
| 32 | +```typescript |
| 33 | +const map = new SqliteMap(":memory:") |
| 34 | +``` |
| 35 | + |
| 36 | +## API |
| 37 | + |
| 38 | +### Constructor |
| 39 | + |
| 40 | +```typescript |
| 41 | +new SqliteMap<K extends string, V>(path: string, options?: DatabaseSyncOptions) |
| 42 | +``` |
| 43 | + |
| 44 | +| Parameter | Type | Description | |
| 45 | +| --------- | --------------------- | --------------------------------------------- | |
| 46 | +| `path` | `string` | Path to SQLite database file, or `":memory:"` | |
| 47 | +| `options` | `DatabaseSyncOptions` | Optional Node.js `DatabaseSync` options | |
| 48 | + |
| 49 | +### Core Methods |
| 50 | + |
| 51 | +#### `set(key, value): this` |
| 52 | +
|
| 53 | +Inserts or replaces a key-value pair. Returns `this` for chaining. |
| 54 | +
|
| 55 | +```typescript |
| 56 | +map.set("a", 1).set("b", 2).set("c", 3) |
| 57 | +``` |
| 58 | +
|
| 59 | +#### `get(key): V | undefined` |
| 60 | +
|
| 61 | +Returns the value for the key, or `undefined` if not found. |
| 62 | +
|
| 63 | +```typescript |
| 64 | +map.get("a") // 1 |
| 65 | +map.get("z") // undefined |
| 66 | +``` |
| 67 | +
|
| 68 | +#### `has(key): boolean` |
| 69 | +
|
| 70 | +Returns `true` if the key exists. |
| 71 | +
|
| 72 | +```typescript |
| 73 | +map.has("a") // true |
| 74 | +``` |
| 75 | +
|
| 76 | +#### `delete(key): boolean` |
| 77 | +
|
| 78 | +Removes the entry. Returns `true` if it existed, `false` otherwise. |
| 79 | +
|
| 80 | +```typescript |
| 81 | +map.delete("a") // true |
| 82 | +map.delete("a") // false |
| 83 | +``` |
| 84 | +
|
| 85 | +#### `clear(): void` |
| 86 | +
|
| 87 | +Removes all entries. |
| 88 | +
|
| 89 | +```typescript |
| 90 | +map.clear() |
| 91 | +map.size // 0 |
| 92 | +``` |
| 93 | +
|
| 94 | +#### `getOrInsert(key, defaultValue): V` |
| 95 | +
|
| 96 | +Returns the existing value if the key exists, otherwise inserts `defaultValue` and returns it. |
| 97 | +
|
| 98 | +```typescript |
| 99 | +map.getOrInsert("count", 0) // inserts 0 and returns 0 |
| 100 | +map.getOrInsert("count", 99) // returns 0 (already exists) |
| 101 | +``` |
| 102 | +
|
| 103 | +#### `getOrInsertComputed(key, callbackFn): V` |
| 104 | +
|
| 105 | +Like `getOrInsert` but the default value is computed lazily via a callback — only called if the key is missing. |
| 106 | +
|
| 107 | +```typescript |
| 108 | +map.getOrInsertComputed("slug", (key) => key.toLowerCase().replace(" ", "-")) |
| 109 | +``` |
| 110 | +
|
| 111 | +### Iteration |
| 112 | +
|
| 113 | +All iteration methods reflect the insertion order of keys. |
| 114 | +
|
| 115 | +#### `keys(): IterableIterator<K>` |
| 116 | +
|
| 117 | +```typescript |
| 118 | +for (const key of map.keys()) console.log(key) // "a", "b", "c" |
| 119 | +console.log([...map.keys()]) // ["a", "b", "c"] |
| 120 | +``` |
| 121 | +
|
| 122 | +#### `values(): IterableIterator<V>` |
| 123 | +
|
| 124 | +```typescript |
| 125 | +console.log([...map.values()]) // [1, 2, 3] |
| 126 | +``` |
| 127 | +
|
| 128 | +#### `entries(): IterableIterator<[K, V]>` |
| 129 | +
|
| 130 | +```typescript |
| 131 | +console.log([...map.entries()]) // [["a", 1], ["b", 2], ["c", 3]] |
| 132 | +``` |
| 133 | +
|
| 134 | +#### `forEach(callback): void` |
| 135 | +
|
| 136 | +```typescript |
| 137 | +map.forEach((value, key, map) => { |
| 138 | + console.log(key, value) |
| 139 | +}) |
| 140 | +``` |
| 141 | +
|
| 142 | +#### `[Symbol.iterator]()` |
| 143 | +
|
| 144 | +Makes the map directly iterable — equivalent to `entries()`. |
| 145 | +
|
| 146 | +```typescript |
| 147 | +for (const [key, value] of map) { |
| 148 | + console.log(key, value) |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +### Properties |
| 153 | + |
| 154 | +#### `size: number` |
| 155 | + |
| 156 | +Returns the number of entries. |
| 157 | + |
| 158 | +```typescript |
| 159 | +map.size // 3 |
| 160 | +``` |
| 161 | + |
| 162 | +### Serialization |
| 163 | + |
| 164 | +#### `toJSON(): Record<string, V>` |
| 165 | + |
| 166 | +Returns a plain object representation. Called automatically by `JSON.stringify`. |
| 167 | + |
| 168 | +```typescript |
| 169 | +map.set("x", 1) |
| 170 | +map.toJSON() // { x: 1 } |
| 171 | +JSON.stringify(map) // '{"x":1}' |
| 172 | +``` |
| 173 | + |
| 174 | +#### `[Symbol.toStringTag]: string` |
| 175 | + |
| 176 | +Returns `"SqliteMap"`. |
| 177 | + |
| 178 | +```typescript |
| 179 | +Object.prototype.toString.call(map) // "[object SqliteMap]" |
| 180 | +``` |
| 181 | + |
| 182 | +## Type Parameters |
| 183 | + |
| 184 | +```typescript |
| 185 | +SqliteMap<K extends string, V> |
| 186 | +``` |
| 187 | + |
| 188 | +| Parameter | Constraint | Description | |
| 189 | +| --------- | ---------- | -------------------------------------- | |
| 190 | +| `K` | `string` | Key type — must be a string | |
| 191 | +| `V` | any | Value type — must be JSON-serializable | |
| 192 | + |
| 193 | +> Values are stored as JSON strings internally, so any JSON-serializable value is supported: objects, arrays, numbers, booleans, strings, null. |
| 194 | +
|
| 195 | +## Examples |
| 196 | + |
| 197 | +### Persistent cache |
| 198 | + |
| 199 | +```typescript |
| 200 | +const cache = new SqliteMap<string, { data: unknown; expiresAt: number }>("./cache.db") |
| 201 | + |
| 202 | +cache.getOrInsertComputed("api:users", () => ({ |
| 203 | + data: fetchUsers(), |
| 204 | + expiresAt: Date.now() + 60_000 |
| 205 | +})) |
| 206 | +``` |
| 207 | + |
| 208 | +### Counting |
| 209 | + |
| 210 | +```typescript |
| 211 | +const hits = new SqliteMap<string, number>("./hits.db") |
| 212 | + |
| 213 | +hits.getOrInsert("/home", 0) |
| 214 | +hits.set("/home", (hits.get("/home") ?? 0) + 1) |
| 215 | +``` |
| 216 | + |
| 217 | +### Spreading / converting |
| 218 | + |
| 219 | +```typescript |
| 220 | +// To plain object |
| 221 | +const obj = sqliteMap.toJSON() |
| 222 | + |
| 223 | +// To native Map |
| 224 | +const native = new Map(sqliteMap.entries()) |
| 225 | + |
| 226 | +// From array of entries |
| 227 | +for (const [k, v] of entries) sqliteMap.set(k, v) |
| 228 | +``` |
| 229 | + |
| 230 | +## Differences from native `Map` |
| 231 | + |
| 232 | +| Feature | `Map` | `SqliteMap` | |
| 233 | +| ---------------- | ----------------- | ----------------- | |
| 234 | +| Persistence | ❌ In-memory only | ✅ File-backed | |
| 235 | +| Key type | Any | `string` only | |
| 236 | +| Value type | Any | JSON-serializable | |
| 237 | +| Iteration order | Insertion order | Insertion order | |
| 238 | +| `JSON.stringify` | `{}` (empty) | ✅ Full object | |
| 239 | + |
| 240 | +## License |
| 241 | + |
| 242 | +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. |
| 243 | + |
| 244 | +## Links & Community |
| 245 | + |
| 246 | +| Resource | URL | |
| 247 | +| ----------------- | ----------------------------------------------- | |
| 248 | +| GitHub repository | https://github.com/xcfio/node-sqlite-map | |
| 249 | +| npm package | https://www.npmjs.com/package/node-sqlite-map | |
| 250 | +| Homepage | https://github.com/xcfio/node-sqlite-map#readme | |
| 251 | +| Bug reports | https://github.com/xcfio/node-sqlite-map/issues | |
| 252 | + |
| 253 | +### Discord |
| 254 | + |
| 255 | +Join the community on Discord for help, and discussion: |
| 256 | + |
| 257 | +**https://discord.gg/FaCCaFM74Q** |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +Made with ❤️ by [xcfio](https://github.com/xcfio) |
0 commit comments