├── .commitlintrc.json ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── adapters │ ├── Memory.test.ts │ ├── Memory.ts │ ├── browser │ │ ├── LocalStorage.ts │ │ ├── SessionStorage.ts │ │ ├── WebStorage.test.ts │ │ └── WebStorage.ts │ └── node │ │ ├── DataFile.ts │ │ ├── JSONFile.test.ts │ │ ├── JSONFile.ts │ │ ├── TextFile.test.ts │ │ └── TextFile.ts ├── browser.ts ├── core │ ├── Low.test.ts │ └── Low.ts ├── examples │ ├── README.md │ ├── browser.ts │ ├── cli.ts │ ├── in-memory.ts │ └── server.ts ├── index.ts ├── node.ts └── presets │ ├── browser.ts │ └── node.ts └── tsconfig.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@typicode", 3 | "env":{ 4 | "browser": true, 5 | "node": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts linguist-language=JavaScript 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: typicode -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run build --if-present 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | npm test 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By contributing, you agree to release your modifications under the MIT 2 | license (see the file LICENSE-MIT). 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 typicode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lowdb [![](http://img.shields.io/npm/dm/lowdb.svg?style=flat)](https://www.npmjs.org/package/lowdb) [![Node.js CI](https://github.com/typicode/lowdb/actions/workflows/node.js.yml/badge.svg)](https://github.com/typicode/lowdb/actions/workflows/node.js.yml) 2 | 3 | > Simple to use type-safe local JSON database 🦉 4 | > 5 | > If you know JavaScript, you know how to use lowdb. 6 | 7 | Read or create `db.json` 8 | 9 | ```js 10 | const db = await JSONFilePreset('db.json', { posts: [] }) 11 | ``` 12 | 13 | Use plain JavaScript to change data 14 | 15 | ```js 16 | const post = { id: 1, title: 'lowdb is awesome', views: 100 } 17 | 18 | // In two steps 19 | db.data.posts.push(post) 20 | await db.write() 21 | 22 | // Or in one 23 | await db.update(({ posts }) => posts.push(post)) 24 | ``` 25 | 26 | ```js 27 | // db.json 28 | { 29 | "posts": [ 30 | { "id": 1, "title": "lowdb is awesome", "views": 100 } 31 | ] 32 | } 33 | ``` 34 | 35 | In the same spirit, query using native `Array` functions: 36 | 37 | ```js 38 | const { posts } = db.data 39 | 40 | posts.at(0) // First post 41 | posts.filter((post) => post.title.includes('lowdb')) // Filter by title 42 | posts.find((post) => post.id === 1) // Find by id 43 | posts.toSorted((a, b) => a.views - b.views) // Sort by views 44 | ``` 45 | 46 | It's that simple. `db.data` is just a JavaScript object, no magic. 47 | 48 | ## Sponsors 49 | 50 |
51 |
52 | 53 |

54 | 55 | 56 | 57 |

58 | 59 |
60 |
61 | 62 | [Become a sponsor and have your company logo here](https://github.com/sponsors/typicode) 👉 [GitHub Sponsors](https://github.com/sponsors/typicode) 63 | 64 | ## Features 65 | 66 | - **Lightweight** 67 | - **Minimalist** 68 | - **TypeScript** 69 | - **Plain JavaScript** 70 | - Safe atomic writes 71 | - Hackable: 72 | - Change storage, file format (JSON, YAML, ...) or add encryption via [adapters](#adapters) 73 | - Extend it with lodash, ramda, ... for super powers! 74 | - Automatically switches to fast in-memory mode during tests 75 | 76 | ## Install 77 | 78 | ```sh 79 | npm install lowdb 80 | ``` 81 | 82 | ## Usage 83 | 84 | _Lowdb is a pure ESM package. If you're having trouble using it in your project, please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)._ 85 | 86 | ```js 87 | import { JSONFilePreset } from 'lowdb/node' 88 | 89 | // Read or create db.json 90 | const defaultData = { posts: [] } 91 | const db = await JSONFilePreset('db.json', defaultData) 92 | 93 | // Update db.json 94 | await db.update(({ posts }) => posts.push('hello world')) 95 | 96 | // Alternatively you can call db.write() explicitely later 97 | // to write to db.json 98 | db.data.posts.push('hello world') 99 | await db.write() 100 | ``` 101 | 102 | ```js 103 | // db.json 104 | { 105 | "posts": [ "hello world" ] 106 | } 107 | ``` 108 | 109 | ### TypeScript 110 | 111 | You can use TypeScript to check your data types. 112 | 113 | ```ts 114 | type Data = { 115 | messages: string[] 116 | } 117 | 118 | const defaultData: Data = { messages: [] } 119 | const db = await JSONPreset('db.json', defaultData) 120 | 121 | db.data.messages.push('foo') // ✅ Success 122 | db.data.messages.push(1) // ❌ TypeScript error 123 | ``` 124 | 125 | ### Lodash 126 | 127 | You can extend lowdb with Lodash (or other libraries). To be able to extend it, we're not using `JSONPreset` here. Instead, we're using lower components. 128 | 129 | ```ts 130 | import { Low } from 'lowdb' 131 | import { JSONFile } from 'lowdb/node' 132 | import lodash from 'lodash' 133 | 134 | type Post = { 135 | id: number 136 | title: string 137 | } 138 | 139 | type Data = { 140 | posts: Post[] 141 | } 142 | 143 | // Extend Low class with a new `chain` field 144 | class LowWithLodash extends Low { 145 | chain: lodash.ExpChain = lodash.chain(this).get('data') 146 | } 147 | 148 | const defaultData: Data = { 149 | posts: [], 150 | } 151 | const adapter = new JSONFile('db.json', defaultData) 152 | 153 | const db = new LowWithLodash(adapter) 154 | await db.read() 155 | 156 | // Instead of db.data use db.chain to access lodash API 157 | const post = db.chain.get('posts').find({ id: 1 }).value() // Important: value() must be called to execute chain 158 | ``` 159 | 160 | ### CLI, Server, Browser and in tests usage 161 | 162 | See [`src/examples/`](src/examples) directory. 163 | 164 | ## API 165 | 166 | ### Presets 167 | 168 | Lowdb provides four presets for common cases. 169 | 170 | - `JSONFilePreset(filename, defaultData)` 171 | - `JSONFileSyncPreset(filename, defaultData)` 172 | - `LocalStoragePreset(name, defaultData)` 173 | - `SessionStoragePreset(name, defaultData)` 174 | 175 | See [`src/examples/`](src/examples) directory for usage. 176 | 177 | Lowdb is extremely flexible, if you need to extend it or modify its behavior, use the classes and adapters below instead of the presets. 178 | 179 | ### Classes 180 | 181 | Lowdb has two classes (for asynchronous and synchronous adapters). 182 | 183 | #### `new Low(adapter, defaultData)` 184 | 185 | ```js 186 | import { Low } from 'lowdb' 187 | import { JSONFile } from 'lowdb/node' 188 | 189 | const db = new Low(new JSONFile('file.json'), {}) 190 | await db.read() 191 | await db.write() 192 | ``` 193 | 194 | #### `new LowSync(adapterSync, defaultData)` 195 | 196 | ```js 197 | import { LowSync } from 'lowdb' 198 | import { JSONFileSync } from 'lowdb/node' 199 | 200 | const db = new LowSync(new JSONFileSync('file.json'), {}) 201 | db.read() 202 | db.write() 203 | ``` 204 | 205 | ### Methods 206 | 207 | #### `db.read()` 208 | 209 | Calls `adapter.read()` and sets `db.data`. 210 | 211 | **Note:** `JSONFile` and `JSONFileSync` adapters will set `db.data` to `null` if file doesn't exist. 212 | 213 | ```js 214 | db.data // === null 215 | db.read() 216 | db.data // !== null 217 | ``` 218 | 219 | #### `db.write()` 220 | 221 | Calls `adapter.write(db.data)`. 222 | 223 | ```js 224 | db.data = { posts: [] } 225 | db.write() // file.json will be { posts: [] } 226 | db.data = {} 227 | db.write() // file.json will be {} 228 | ``` 229 | 230 | #### `db.update(fn)` 231 | 232 | Calls `fn()` then `db.write()`. 233 | 234 | ```js 235 | db.update((data) => { 236 | // make changes to data 237 | // ... 238 | }) 239 | // files.json will be updated 240 | ``` 241 | 242 | ### Properties 243 | 244 | #### `db.data` 245 | 246 | Holds your db content. If you're using the adapters coming with lowdb, it can be any type supported by [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). 247 | 248 | For example: 249 | 250 | ```js 251 | db.data = 'string' 252 | db.data = [1, 2, 3] 253 | db.data = { key: 'value' } 254 | ``` 255 | 256 | ## Adapters 257 | 258 | ### Lowdb adapters 259 | 260 | #### `JSONFile` `JSONFileSync` 261 | 262 | Adapters for reading and writing JSON files. 263 | 264 | ```js 265 | import { JSONFile, JSONFileSync } from 'lowdb/node' 266 | 267 | new Low(new JSONFile(filename), {}) 268 | new LowSync(new JSONFileSync(filename), {}) 269 | ``` 270 | 271 | #### `Memory` `MemorySync` 272 | 273 | In-memory adapters. Useful for speeding up unit tests. See [`src/examples/`](src/examples) directory. 274 | 275 | ```js 276 | import { Memory, MemorySync } from 'lowdb' 277 | 278 | new Low(new Memory(), {}) 279 | new LowSync(new MemorySync(), {}) 280 | ``` 281 | 282 | #### `LocalStorage` `SessionStorage` 283 | 284 | Synchronous adapter for `window.localStorage` and `window.sessionStorage`. 285 | 286 | ```js 287 | import { LocalStorage, SessionStorage } from 'lowdb/browser' 288 | new LowSync(new LocalStorage(name), {}) 289 | new LowSync(new SessionStorage(name), {}) 290 | ``` 291 | 292 | ### Utility adapters 293 | 294 | #### `TextFile` `TextFileSync` 295 | 296 | Adapters for reading and writing text. Useful for creating custom adapters. 297 | 298 | #### `DataFile` `DataFileSync` 299 | 300 | Adapters for easily supporting other data formats or adding behaviors (encrypt, compress...). 301 | 302 | ```js 303 | import { DataFile } from 'lowdb/node' 304 | new DataFile(filename, { 305 | parse: YAML.parse, 306 | stringify: YAML.stringify 307 | }) 308 | new DataFile(filename, { 309 | parse: (data) => { decypt(JSON.parse(data)) }, 310 | stringify: (str) => { encrypt(JSON.stringify(str)) } 311 | }) 312 | ``` 313 | 314 | ### Third-party adapters 315 | 316 | If you've published an adapter for lowdb, feel free to create a PR to add it here. 317 | 318 | ### Writing your own adapter 319 | 320 | You may want to create an adapter to write `db.data` to YAML, XML, encrypt data, a remote storage, ... 321 | 322 | An adapter is a simple class that just needs to expose two methods: 323 | 324 | ```js 325 | class AsyncAdapter { 326 | read() { 327 | /* ... */ 328 | } // should return Promise 329 | write(data) { 330 | /* ... */ 331 | } // should return Promise 332 | } 333 | 334 | class SyncAdapter { 335 | read() { 336 | /* ... */ 337 | } // should return data 338 | write(data) { 339 | /* ... */ 340 | } // should return nothing 341 | } 342 | ``` 343 | 344 | For example, let's say you have some async storage and want to create an adapter for it: 345 | 346 | ```js 347 | import { Low } from 'lowdb' 348 | import { api } from './AsyncStorage' 349 | 350 | class CustomAsyncAdapter { 351 | // Optional: your adapter can take arguments 352 | constructor(args) { 353 | // ... 354 | } 355 | 356 | async read() { 357 | const data = await api.read() 358 | return data 359 | } 360 | 361 | async write(data) { 362 | await api.write(data) 363 | } 364 | } 365 | 366 | const adapter = new CustomAsyncAdapter() 367 | const db = new Low(adapter, {}) 368 | ``` 369 | 370 | See [`src/adapters/`](src/adapters) for more examples. 371 | 372 | #### Custom serialization 373 | 374 | To create an adapter for another format than JSON, you can use `TextFile` or `TextFileSync`. 375 | 376 | For example: 377 | 378 | ```js 379 | import { Adapter, Low } from 'lowdb' 380 | import { TextFile } from 'lowdb/node' 381 | import YAML from 'yaml' 382 | 383 | class YAMLFile { 384 | constructor(filename) { 385 | this.adapter = new TextFile(filename) 386 | } 387 | 388 | async read() { 389 | const data = await this.adapter.read() 390 | if (data === null) { 391 | return null 392 | } else { 393 | return YAML.parse(data) 394 | } 395 | } 396 | 397 | write(obj) { 398 | return this.adapter.write(YAML.stringify(obj)) 399 | } 400 | } 401 | 402 | const adapter = new YAMLFile('file.yaml') 403 | const db = new Low(adapter, {}) 404 | ``` 405 | 406 | ## Limits 407 | 408 | Lowdb doesn't support Node's cluster module. 409 | 410 | If you have large JavaScript objects (`~10-100MB`) you may hit some performance issues. This is because whenever you call `db.write`, the whole `db.data` is serialized using `JSON.stringify` and written to storage. 411 | 412 | Depending on your use case, this can be fine or not. It can be mitigated by doing batch operations and calling `db.write` only when you need it. 413 | 414 | If you plan to scale, it's highly recommended to use databases like PostgreSQL or MongoDB instead. 415 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lowdb", 3 | "version": "7.0.1", 4 | "description": "Tiny local JSON database for Node, Electron and the browser", 5 | "keywords": [ 6 | "database", 7 | "db", 8 | "electron", 9 | "embed", 10 | "embedded", 11 | "flat", 12 | "JSON", 13 | "local", 14 | "localStorage", 15 | "sessionStorage", 16 | "browser", 17 | "esm" 18 | ], 19 | "homepage": "https://github.com/typicode/lowdb#readme", 20 | "bugs": { 21 | "url": "https://github.com/typicode/lowdb/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/typicode/lowdb.git" 26 | }, 27 | "funding": "https://github.com/sponsors/typicode", 28 | "license": "MIT", 29 | "author": "Typicode ", 30 | "type": "module", 31 | "exports": { 32 | ".": "./lib/index.js", 33 | "./node": "./lib/node.js", 34 | "./browser": "./lib/browser.js" 35 | }, 36 | "types": "./lib", 37 | "typesVersions": { 38 | "*": { 39 | "node": [ 40 | "lib/node.d.ts" 41 | ], 42 | "browser": [ 43 | "lib/browser.d.ts" 44 | ] 45 | } 46 | }, 47 | "files": [ 48 | "lib", 49 | "!lib/examples/**/*", 50 | "!lib/**/*.test.*" 51 | ], 52 | "scripts": { 53 | "test": "node --import tsx/esm --test src/**/*.test.ts src/**/**/*.test.ts", 54 | "lint": "eslint src --ext .ts --ignore-path .gitignore", 55 | "build": "del-cli lib && tsc", 56 | "prepublishOnly": "npm run build", 57 | "postversion": "git push --follow-tags && npm publish", 58 | "prepare": "husky install" 59 | }, 60 | "dependencies": { 61 | "steno": "^4.0.2" 62 | }, 63 | "devDependencies": { 64 | "@commitlint/cli": "^18.4.3", 65 | "@commitlint/config-conventional": "^18.4.3", 66 | "@commitlint/prompt-cli": "^18.4.3", 67 | "@sindresorhus/tsconfig": "^5.0.0", 68 | "@types/express": "^4.17.21", 69 | "@types/lodash": "^4.14.202", 70 | "@types/node": "^20.10.5", 71 | "@typicode/eslint-config": "^1.2.0", 72 | "del-cli": "^5.1.0", 73 | "eslint": "^8.56.0", 74 | "express-async-handler": "^1.2.0", 75 | "husky": "^8.0.3", 76 | "lodash": "^4.17.21", 77 | "tempy": "^3.1.0", 78 | "ts-node": "^10.9.2", 79 | "tsx": "^4.7.0", 80 | "typescript": "^5.3.3" 81 | }, 82 | "engines": { 83 | "node": ">=18" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/adapters/Memory.test.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual, equal } from 'node:assert/strict' 2 | import test from 'node:test' 3 | 4 | import { Memory, MemorySync } from './Memory.js' 5 | 6 | await test('Memory', async () => { 7 | const obj = { a: 1 } 8 | 9 | const memory = new Memory() 10 | 11 | // Null by default 12 | equal(await memory.read(), null) 13 | 14 | // Write 15 | equal(await memory.write(obj), undefined) 16 | 17 | // Read 18 | deepEqual(await memory.read(), obj) 19 | }) 20 | 21 | await test('MemorySync', () => { 22 | const obj = { a: 1 } 23 | 24 | const memory = new MemorySync() 25 | 26 | // Null by default 27 | equal(memory.read(), null) 28 | 29 | // Write 30 | equal(memory.write(obj), undefined) 31 | 32 | // Read 33 | deepEqual(memory.read(), obj) 34 | }) 35 | -------------------------------------------------------------------------------- /src/adapters/Memory.ts: -------------------------------------------------------------------------------- 1 | import { Adapter, SyncAdapter } from '../core/Low.js' 2 | 3 | export class Memory implements Adapter { 4 | #data: T | null = null 5 | 6 | read(): Promise { 7 | return Promise.resolve(this.#data) 8 | } 9 | 10 | write(obj: T): Promise { 11 | this.#data = obj 12 | return Promise.resolve() 13 | } 14 | } 15 | 16 | export class MemorySync implements SyncAdapter { 17 | #data: T | null = null 18 | 19 | read(): T | null { 20 | return this.#data || null 21 | } 22 | 23 | write(obj: T): void { 24 | this.#data = obj 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/adapters/browser/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { WebStorage } from './WebStorage.js' 2 | 3 | export class LocalStorage extends WebStorage { 4 | constructor(key: string) { 5 | super(key, localStorage) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/adapters/browser/SessionStorage.ts: -------------------------------------------------------------------------------- 1 | import { WebStorage } from './WebStorage.js' 2 | 3 | export class SessionStorage extends WebStorage { 4 | constructor(key: string) { 5 | super(key, sessionStorage) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/adapters/browser/WebStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual, equal } from 'node:assert/strict' 2 | import test from 'node:test' 3 | 4 | import { WebStorage } from './WebStorage.js' 5 | 6 | const storage: { [key: string]: string } = {} 7 | 8 | // Mock localStorage 9 | const mockStorage = () => ({ 10 | getItem: (key: string): string | null => storage[key] || null, 11 | setItem: (key: string, data: string) => (storage[key] = data), 12 | length: 1, 13 | removeItem() { 14 | return 15 | }, 16 | clear() { 17 | return 18 | }, 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | key(_: number): string { 21 | return '' 22 | }, 23 | }) 24 | global.localStorage = mockStorage() 25 | global.sessionStorage = mockStorage() 26 | 27 | await test('localStorage', () => { 28 | const obj = { a: 1 } 29 | const storage = new WebStorage('key', localStorage) 30 | 31 | // Write 32 | equal(storage.write(obj), undefined) 33 | 34 | // Read 35 | deepEqual(storage.read(), obj) 36 | }) 37 | 38 | await test('sessionStorage', () => { 39 | const obj = { a: 1 } 40 | const storage = new WebStorage('key', sessionStorage) 41 | 42 | // Write 43 | equal(storage.write(obj), undefined) 44 | 45 | // Read 46 | deepEqual(storage.read(), obj) 47 | }) 48 | -------------------------------------------------------------------------------- /src/adapters/browser/WebStorage.ts: -------------------------------------------------------------------------------- 1 | import { SyncAdapter } from '../../core/Low.js' 2 | 3 | export class WebStorage implements SyncAdapter { 4 | #key: string 5 | #storage: Storage 6 | 7 | constructor(key: string, storage: Storage) { 8 | this.#key = key 9 | this.#storage = storage 10 | } 11 | 12 | read(): T | null { 13 | const value = this.#storage.getItem(this.#key) 14 | 15 | if (value === null) { 16 | return null 17 | } 18 | 19 | return JSON.parse(value) as T 20 | } 21 | 22 | write(obj: T): void { 23 | this.#storage.setItem(this.#key, JSON.stringify(obj)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/adapters/node/DataFile.ts: -------------------------------------------------------------------------------- 1 | import { PathLike } from 'fs' 2 | 3 | import { Adapter, SyncAdapter } from '../../core/Low.js' 4 | import { TextFile, TextFileSync } from './TextFile.js' 5 | 6 | export class DataFile implements Adapter { 7 | #adapter: TextFile 8 | #parse: (str: string) => T 9 | #stringify: (data: T) => string 10 | 11 | constructor( 12 | filename: PathLike, 13 | { 14 | parse, 15 | stringify, 16 | }: { 17 | parse: (str: string) => T 18 | stringify: (data: T) => string 19 | }, 20 | ) { 21 | this.#adapter = new TextFile(filename) 22 | this.#parse = parse 23 | this.#stringify = stringify 24 | } 25 | 26 | async read(): Promise { 27 | const data = await this.#adapter.read() 28 | if (data === null) { 29 | return null 30 | } else { 31 | return this.#parse(data) 32 | } 33 | } 34 | 35 | write(obj: T): Promise { 36 | return this.#adapter.write(this.#stringify(obj)) 37 | } 38 | } 39 | 40 | export class DataFileSync implements SyncAdapter { 41 | #adapter: TextFileSync 42 | #parse: (str: string) => T 43 | #stringify: (data: T) => string 44 | 45 | constructor( 46 | filename: PathLike, 47 | { 48 | parse, 49 | stringify, 50 | }: { 51 | parse: (str: string) => T 52 | stringify: (data: T) => string 53 | }, 54 | ) { 55 | this.#adapter = new TextFileSync(filename) 56 | this.#parse = parse 57 | this.#stringify = stringify 58 | } 59 | 60 | read(): T | null { 61 | const data = this.#adapter.read() 62 | if (data === null) { 63 | return null 64 | } else { 65 | return this.#parse(data) 66 | } 67 | } 68 | 69 | write(obj: T): void { 70 | this.#adapter.write(this.#stringify(obj)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/adapters/node/JSONFile.test.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual, equal } from 'node:assert/strict' 2 | import test from 'node:test' 3 | 4 | import { temporaryFile } from 'tempy' 5 | 6 | import { JSONFile, JSONFileSync } from './JSONFile.js' 7 | 8 | type Data = { 9 | a: number 10 | } 11 | 12 | await test('JSONFile', async () => { 13 | const obj = { a: 1 } 14 | const file = new JSONFile(temporaryFile()) 15 | 16 | // Null if file doesn't exist 17 | equal(await file.read(), null) 18 | 19 | // Write 20 | equal(await file.write(obj), undefined) 21 | 22 | // Read 23 | deepEqual(await file.read(), obj) 24 | }) 25 | 26 | await test('JSONFileSync', () => { 27 | const obj = { a: 1 } 28 | const file = new JSONFileSync(temporaryFile()) 29 | 30 | // Null if file doesn't exist 31 | equal(file.read(), null) 32 | 33 | // Write 34 | equal(file.write(obj), undefined) 35 | 36 | // Read 37 | deepEqual(file.read(), obj) 38 | }) 39 | -------------------------------------------------------------------------------- /src/adapters/node/JSONFile.ts: -------------------------------------------------------------------------------- 1 | import { PathLike } from 'fs' 2 | 3 | import { DataFile, DataFileSync } from './DataFile.js' 4 | 5 | export class JSONFile extends DataFile { 6 | constructor(filename: PathLike) { 7 | super(filename, { 8 | parse: JSON.parse, 9 | stringify: (data: T) => JSON.stringify(data, null, 2), 10 | }) 11 | } 12 | } 13 | 14 | export class JSONFileSync extends DataFileSync { 15 | constructor(filename: PathLike) { 16 | super(filename, { 17 | parse: JSON.parse, 18 | stringify: (data: T) => JSON.stringify(data, null, 2), 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/adapters/node/TextFile.test.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual, equal } from 'node:assert/strict' 2 | import test from 'node:test' 3 | 4 | import { temporaryFile } from 'tempy' 5 | 6 | import { TextFile, TextFileSync } from './TextFile.js' 7 | 8 | await test('TextFile', async () => { 9 | const str = 'foo' 10 | const file = new TextFile(temporaryFile()) 11 | 12 | // Null if file doesn't exist 13 | equal(await file.read(), null) 14 | 15 | // Write 16 | equal(await file.write(str), undefined) 17 | 18 | // Read 19 | deepEqual(await file.read(), str) 20 | }) 21 | 22 | await test('TextFileSync', () => { 23 | const str = 'foo' 24 | const file = new TextFileSync(temporaryFile()) 25 | 26 | // Null if file doesn't exist 27 | equal(file.read(), null) 28 | 29 | // Write 30 | equal(file.write(str), undefined) 31 | 32 | // Read 33 | deepEqual(file.read(), str) 34 | }) 35 | 36 | await test('RaceCondition', async () => { 37 | const file = new TextFile(temporaryFile()) 38 | const promises: Promise[] = [] 39 | 40 | let i = 0 41 | for (; i <= 100; i++) { 42 | promises.push(file.write(String(i))) 43 | } 44 | 45 | await Promise.all(promises) 46 | 47 | equal(await file.read(), String(i - 1)) 48 | }) 49 | -------------------------------------------------------------------------------- /src/adapters/node/TextFile.ts: -------------------------------------------------------------------------------- 1 | import { PathLike, readFileSync, renameSync, writeFileSync } from 'node:fs' 2 | import { readFile } from 'node:fs/promises' 3 | import path from 'node:path' 4 | 5 | import { Writer } from 'steno' 6 | 7 | import { Adapter, SyncAdapter } from '../../core/Low.js' 8 | 9 | export class TextFile implements Adapter { 10 | #filename: PathLike 11 | #writer: Writer 12 | 13 | constructor(filename: PathLike) { 14 | this.#filename = filename 15 | this.#writer = new Writer(filename) 16 | } 17 | 18 | async read(): Promise { 19 | let data 20 | 21 | try { 22 | data = await readFile(this.#filename, 'utf-8') 23 | } catch (e) { 24 | if ((e as NodeJS.ErrnoException).code === 'ENOENT') { 25 | return null 26 | } 27 | throw e 28 | } 29 | 30 | return data 31 | } 32 | 33 | write(str: string): Promise { 34 | return this.#writer.write(str) 35 | } 36 | } 37 | 38 | export class TextFileSync implements SyncAdapter { 39 | #tempFilename: PathLike 40 | #filename: PathLike 41 | 42 | constructor(filename: PathLike) { 43 | this.#filename = filename 44 | const f = filename.toString() 45 | this.#tempFilename = path.join(path.dirname(f), `.${path.basename(f)}.tmp`) 46 | } 47 | 48 | read(): string | null { 49 | let data 50 | 51 | try { 52 | data = readFileSync(this.#filename, 'utf-8') 53 | } catch (e) { 54 | if ((e as NodeJS.ErrnoException).code === 'ENOENT') { 55 | return null 56 | } 57 | throw e 58 | } 59 | 60 | return data 61 | } 62 | 63 | write(str: string): void { 64 | writeFileSync(this.#tempFilename, str) 65 | renameSync(this.#tempFilename, this.#filename) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters/browser/LocalStorage.js' 2 | export * from './adapters/browser/SessionStorage.js' 3 | export * from './presets/browser.js' 4 | -------------------------------------------------------------------------------- /src/core/Low.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { deepEqual, equal, throws } from 'node:assert/strict' 3 | import fs from 'node:fs' 4 | import test from 'node:test' 5 | 6 | import lodash from 'lodash' 7 | import { temporaryFile } from 'tempy' 8 | 9 | import { Memory } from '../adapters/Memory.js' 10 | import { JSONFile, JSONFileSync } from '../adapters/node/JSONFile.js' 11 | import { Low, LowSync } from './Low.js' 12 | 13 | type Data = { 14 | a?: number 15 | b?: number 16 | } 17 | 18 | function createJSONFile(obj: unknown): string { 19 | const file = temporaryFile() 20 | fs.writeFileSync(file, JSON.stringify(obj)) 21 | return file 22 | } 23 | 24 | function readJSONFile(file: string): unknown { 25 | return JSON.parse(fs.readFileSync(file).toString()) 26 | } 27 | 28 | await test('CheckArgs', () => { 29 | const adapter = new Memory() 30 | // Ignoring TypeScript error and pass incorrect argument 31 | // @ts-ignore 32 | throws(() => new Low()) 33 | // @ts-ignore 34 | throws(() => new LowSync()) 35 | // @ts-ignore 36 | throws(() => new Low(adapter)) 37 | // @ts-ignore 38 | throws(() => new LowSync(adapter)) 39 | }) 40 | 41 | await test('Low', async () => { 42 | // Create JSON file 43 | const obj = { a: 1 } 44 | const file = createJSONFile(obj) 45 | 46 | // Init 47 | const defaultData: Data = {} 48 | const adapter = new JSONFile(file) 49 | const low = new Low(adapter, defaultData) 50 | await low.read() 51 | 52 | // Data should equal file content 53 | deepEqual(low.data, obj) 54 | 55 | // Write new data 56 | const newObj = { b: 2 } 57 | low.data = newObj 58 | await low.write() 59 | 60 | // File content should equal new data 61 | deepEqual(readJSONFile(file), newObj) 62 | 63 | // Write using update() 64 | await low.update((data) => { 65 | data.b = 3 66 | }) 67 | deepEqual(readJSONFile(file), { b: 3 }) 68 | }) 69 | 70 | await test('LowSync', () => { 71 | // Create JSON file 72 | const obj = { a: 1 } 73 | const file = createJSONFile(obj) 74 | 75 | // Init 76 | const defaultData: Data = {} 77 | const adapter = new JSONFileSync(file) 78 | const low = new LowSync(adapter, defaultData) 79 | low.read() 80 | 81 | // Data should equal file content 82 | deepEqual(low.data, obj) 83 | 84 | // Write new data 85 | const newObj = { b: 2 } 86 | low.data = newObj 87 | low.write() 88 | 89 | // File content should equal new data 90 | deepEqual(readJSONFile(file), newObj) 91 | 92 | // Write using update() 93 | low.update((data) => { 94 | data.b = 3 95 | }) 96 | deepEqual(readJSONFile(file), { b: 3 }) 97 | }) 98 | 99 | await test('Lodash', async () => { 100 | // Extend with lodash 101 | class LowWithLodash extends Low { 102 | chain: lodash.ExpChain = lodash.chain(this).get('data') 103 | } 104 | 105 | // Create JSON file 106 | const obj = { todos: ['foo', 'bar'] } 107 | const file = createJSONFile(obj) 108 | 109 | // Init 110 | const defaultData = { todos: [] } 111 | const adapter = new JSONFile(file) 112 | const low = new LowWithLodash(adapter, defaultData) 113 | await low.read() 114 | 115 | // Use lodash 116 | const firstTodo = low.chain.get('todos').first().value() 117 | 118 | equal(firstTodo, 'foo') 119 | }) 120 | -------------------------------------------------------------------------------- /src/core/Low.ts: -------------------------------------------------------------------------------- 1 | export interface Adapter { 2 | read: () => Promise 3 | write: (data: T) => Promise 4 | } 5 | 6 | export interface SyncAdapter { 7 | read: () => T | null 8 | write: (data: T) => void 9 | } 10 | 11 | function checkArgs(adapter: unknown, defaultData: unknown) { 12 | if (adapter === undefined) throw new Error('lowdb: missing adapter') 13 | if (defaultData === undefined) throw new Error('lowdb: missing default data') 14 | } 15 | 16 | export class Low { 17 | adapter: Adapter 18 | data: T 19 | 20 | constructor(adapter: Adapter, defaultData: T) { 21 | checkArgs(adapter, defaultData) 22 | this.adapter = adapter 23 | this.data = defaultData 24 | } 25 | 26 | async read(): Promise { 27 | const data = await this.adapter.read() 28 | if (data) this.data = data 29 | } 30 | 31 | async write(): Promise { 32 | if (this.data) await this.adapter.write(this.data) 33 | } 34 | 35 | async update(fn: (data: T) => unknown): Promise { 36 | fn(this.data) 37 | await this.write() 38 | } 39 | } 40 | 41 | export class LowSync { 42 | adapter: SyncAdapter 43 | data: T 44 | 45 | constructor(adapter: SyncAdapter, defaultData: T) { 46 | checkArgs(adapter, defaultData) 47 | this.adapter = adapter 48 | this.data = defaultData 49 | } 50 | 51 | read(): void { 52 | const data = this.adapter.read() 53 | if (data) this.data = data 54 | } 55 | 56 | write(): void { 57 | if (this.data) this.adapter.write(this.data) 58 | } 59 | 60 | update(fn: (data: T) => unknown): void { 61 | fn(this.data) 62 | this.write() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [cli.ts](./cli.ts) - Simple CLI using JSONFileSync adapter 4 | - [server.ts](./server.ts) - Express example using JSONFile adapter 5 | - [browser.ts](./browser.ts) - LocalStorage adapter example 6 | - [in-memory.ts](./in-memory.ts) - Example showing how to use in-memory adapter to write fast tests 7 | -------------------------------------------------------------------------------- /src/examples/browser.ts: -------------------------------------------------------------------------------- 1 | import { LocalStoragePreset } from '../presets/browser.js' 2 | 3 | type Data = { 4 | messages: string[] 5 | } 6 | 7 | const defaultData: Data = { messages: [] } 8 | const db = LocalStoragePreset('db', defaultData) 9 | 10 | db.update(({ messages }) => messages.push('foo')) 11 | -------------------------------------------------------------------------------- /src/examples/cli.ts: -------------------------------------------------------------------------------- 1 | import { JSONFileSyncPreset } from '../presets/node.js' 2 | 3 | type Data = { 4 | messages: string[] 5 | } 6 | 7 | const message = process.argv[2] || '' 8 | 9 | const defaultData: Data = { messages: [] } 10 | const db = JSONFileSyncPreset('file.json', defaultData) 11 | 12 | db.update(({ messages }) => messages.push(message)) 13 | -------------------------------------------------------------------------------- /src/examples/in-memory.ts: -------------------------------------------------------------------------------- 1 | // With this adapter, calling `db.write()` will do nothing. 2 | // One use case for this adapter can be for tests. 3 | import { LowSync, MemorySync, SyncAdapter } from '../index.js' 4 | import { JSONFileSync } from '../node.js' 5 | 6 | declare global { 7 | // eslint-disable-next-line @typescript-eslint/no-namespace 8 | namespace NodeJS { 9 | interface ProcessEnv { 10 | NODE_ENV: 'test' | 'dev' | 'prod' 11 | } 12 | } 13 | } 14 | 15 | type Data = Record 16 | const defaultData: Data = {} 17 | const adapter: SyncAdapter = 18 | process.env.NODE_ENV === 'test' 19 | ? new MemorySync() 20 | : new JSONFileSync('db.json') 21 | 22 | const db = new LowSync(adapter, defaultData) 23 | db.read() 24 | // Rest of your code... 25 | -------------------------------------------------------------------------------- /src/examples/server.ts: -------------------------------------------------------------------------------- 1 | // Note: if you're developing a local server and don't expect to get concurrent requests, 2 | // it can be easier to use `JSONFileSync` adapter. 3 | // But if you need to avoid blocking requests, you can do so by using `JSONFile` adapter. 4 | import express from 'express' 5 | import asyncHandler from 'express-async-handler' 6 | 7 | import { JSONFilePreset } from '../presets/node.js' 8 | 9 | const app = express() 10 | app.use(express.json()) 11 | 12 | type Post = { 13 | id: string 14 | body: string 15 | } 16 | 17 | type Data = { 18 | posts: Post[] 19 | } 20 | 21 | const defaultData: Data = { posts: [] } 22 | const db = await JSONFilePreset('db.json', defaultData) 23 | 24 | // db.data can be destructured to avoid typing `db.data` everywhere 25 | const { posts } = db.data 26 | 27 | app.get('/posts/:id', (req, res) => { 28 | const post = posts.find((p) => p.id === req.params.id) 29 | res.send(post) 30 | }) 31 | 32 | app.post( 33 | '/posts', 34 | asyncHandler(async (req, res) => { 35 | const post = req.body as Post 36 | post.id = String(posts.length + 1) 37 | await db.update(({ posts }) => posts.push(post)) 38 | res.send(post) 39 | }), 40 | ) 41 | 42 | app.listen(3000, () => { 43 | console.log('listening on port 3000') 44 | }) 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters/Memory.js' 2 | export * from './core/Low.js' 3 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters/node/DataFile.js' 2 | export * from './adapters/node/JSONFile.js' 3 | export * from './adapters/node/TextFile.js' 4 | export * from './presets/node.js' 5 | -------------------------------------------------------------------------------- /src/presets/browser.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorage } from '../adapters/browser/LocalStorage.js' 2 | import { SessionStorage } from '../adapters/browser/SessionStorage.js' 3 | import { LowSync } from '../index.js' 4 | 5 | export function LocalStoragePreset( 6 | key: string, 7 | defaultData: Data, 8 | ): LowSync { 9 | const adapter = new LocalStorage(key) 10 | const db = new LowSync(adapter, defaultData) 11 | db.read() 12 | return db 13 | } 14 | 15 | export function SessionStoragePreset( 16 | key: string, 17 | defaultData: Data, 18 | ): LowSync { 19 | const adapter = new SessionStorage(key) 20 | const db = new LowSync(adapter, defaultData) 21 | db.read() 22 | return db 23 | } 24 | -------------------------------------------------------------------------------- /src/presets/node.ts: -------------------------------------------------------------------------------- 1 | import { PathLike } from 'node:fs' 2 | 3 | import { Memory, MemorySync } from '../adapters/Memory.js' 4 | import { JSONFile, JSONFileSync } from '../adapters/node/JSONFile.js' 5 | import { Low, LowSync } from '../core/Low.js' 6 | 7 | export async function JSONFilePreset( 8 | filename: PathLike, 9 | defaultData: Data, 10 | ): Promise> { 11 | const adapter = 12 | process.env.NODE_ENV === 'test' 13 | ? new Memory() 14 | : new JSONFile(filename) 15 | const db = new Low(adapter, defaultData) 16 | await db.read() 17 | return db 18 | } 19 | 20 | export function JSONFileSyncPreset( 21 | filename: PathLike, 22 | defaultData: Data, 23 | ): LowSync { 24 | const adapter = 25 | process.env.NODE_ENV === 'test' 26 | ? new MemorySync() 27 | : new JSONFileSync(filename) 28 | const db = new LowSync(adapter, defaultData) 29 | db.read() 30 | return db 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | } 6 | } 7 | --------------------------------------------------------------------------------