├── .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 [](https://www.npmjs.org/package/lowdb) [](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 |
--------------------------------------------------------------------------------