leaf-db

Small file-based database for node.js

Usage no npm install needed!

<script type="module">
  import leafDb from 'https://cdn.skypack.dev/leaf-db';
</script>

README

leaf-db

leaf-db

leaf-db is a modern, promise-based, strongly-typed, embeddable database for node.js.

Install

$ npm i leaf-db

Note: This package requires Node >=14.5.0

Getting Started

JS

import LeafDB from 'leaf-db'; // ES6
// const LeafDB = require('leaf-db').default // ES5

const db = new LeafDB();

db.insert({ species: 'cat', name: 'whiskers' })
  .then(inserted => console.log(`added ${inserted[0].name} to the database!`))
  .catch(console.error)

TS

import LeafDB from 'leaf-db';

type Document = { species: string, name?: string }

const db = new LeafDB<Document>();
db.insert({ species: 'cat', name: 'whiskers' })
  .then(inserted => console.log(`added ${inserted[0].name} to the database!`))
  .catch(console.error)

API

Database

Create / load

const db = new LeafDB({ name, root, autoload, strict })

  • options.name - Database name
  • options.root - Database root path, will create in-memory if not provided
  • options.disableAutoload - Should database not be loaded on creation
  • options.strict - Should database throw silent errors
// Memory-only database
const db = new Datastore()

// Persistent database with autoload
const db = new Datastore({ root: process.cwd() });

// Persistent database with manual load
const db = new Datastore({ name: 'db', root: process.cwd(), disableAutoload: true })
// Loading is not neccesary, but recommended
// Not loading means the data from file isn't read,
// which can cause data loss when `persist()` is called (as it overwrites the file)
db.load()

Persistence

By default, leaf-db does not write directly to file after operations. To make sure the data is persisted, call persist(), which will write valid data to disk. persist() also cleans out invalid data from memory.

If strict is enabled, persist() will throw an error if corrupted data is found.

Corruption

Calling load() will return an array of corrupted raw data (string), which can be re-inserted before calling persist().

Inserting docs

await db.insert(OneOrMore<NewDoc>) => Promise<Doc[]>

Inserts doc(s) into the database. _id is automatically generated if the _id does not exist.

Fields cannot start with $ (modifier field) or contain . (dot-queries). Values cannot be undefined.

insert() will reject on the first invalid doc if strict is enabled, otherwise invalid docs are ignored.

Insertion takes place after all docs are validated, meaning no data will be inserted if insert() rejects.

leaf-db does not keep track of when docs are inserted, updated or deleted.

Example

const newDoc = {
  crud: 'create',
  data: [{
    field: 1
  }]
}

try {
  const doc = await db.insert(newDoc) // [newDoc]
} catch (err) {
  console.error(err)
}

Finding docs

Basic query

await db.find(Query | string[], Projection) => Promise<Doc[]>

await db.findById(string, Projection) => Promise<Doc>

Find doc(s) matching query. Operators and dot notation are supported and can be mixed together.

// Data
// { _id: 1, type: 'normal', important: false, variants: ['weak', 'strong'] }
// { _id: 2, type: 'normal', important: true, variants: ['weak', 'strong'] }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3 } }

// Find docs matching type 'normal'
// [1, 2, 3] (Doc _id's)
await db.find({ type: 'normal' })

// Find all docs matching type 'normal' and important 'true'
// [2], all fields must match
await db.find({ type: 'normal', important: 'true' })

// Find all docs with variants 'weak'
// [4], note how only 4 matches, even though all entries contain weak
// Array content and order must mach
await db.find({ variant: ['weak'] })

// Find all docs with variants 'strong', 'weak', in that order
// []
await db.find({ variant: ['strong', 'weak'] })

// Find all docs with parent '3'
// [], all keys must be present
await db.find({ properties: { parent: 3 } })
// [4], key order does not matter
await db.find({ properties: { parent: 3, type: 'weak' } })

Dot notation

Dot notation can be used to match nested fields

// Data
// { _id: 1, variants: ['normal', 'strong'], properties: { type: 'weak', parent: 3 } }
// { _id: 2, variants: ['strong', 'normal'], properties: { type: 'weak', parent: 3 } }
// { _id: 3, variants: [{ id: 'strong', properties: [{ type: 'weak' }] }] }

// Find all docs with properties.type 'weak'
// [1, 2]
await db.find({ 'properties.type': 'weak' })

// Find all docs where first entry of variants is `strong`
// [2]
await db.find({ 'variants.0': 'strong' })

// Find all docs where type of first entry of properties of first entry of variants is 'weak'
// [3]
await db.find({ 'variants.0.properties.0.type': 'weak' })

Operators

Operators can be used to create advanced queries. The following operators are supported:

Logic operators

  • $gt - Is greater than
  • $gte - Is greater or equal than
  • $lt - Is less than
  • $lte - Is less or equal than
  • $not - Is not equal

String operators

  • $string - Does string include string
  • $stringStrict - Does string include string, case sensitive

Object operators

  • $keys - Does object have keys

Array operators

These operators will return false if the queries field is not an array

  • $includes - Does array contain value
  • $or - Do any of the queries match

Example

// Data
// { _id: 1, type: 'normal', important: false, variants: ['weak', 'strong'] }
// { _id: 2, type: 'normal', important: true, variants: ['weak', 'strong'] }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3, variants: ['strong'] } }
// { _id: 5, properties: [{ variants: ['weak', 'normal' ] }, { type: 'strong' }] }

// $gt / $gte / $lt / $lte
// [3, 4]
await db.find({ $gt: { _id: 2 } })
// [4], all fields within '$lte' must match
await db.find({ $lte: { _id: 4, 'properties.parent': 3 }})

// $not
// [2, 3, 4, 5]
await db.find({ $not: { _id: 1 } })

// $string
// [1, 2]
await db.find({ $string: { type: 'mal' } })
// []
await db.find({ $string: { type: 'MAL' } })
// [1, 2]
await db.find({ $stringStrict: { type: 'MAL' } })

// $keys
// [1, 2, 3, 4]
await db.find({ $keys: ['type'] })
// [1, 2, 3]
await db.find({ $keys: ['type', 'important'] })

// $includes
// [1, 2, 3, 4]
await db.find({ $includes: { variants: 'weak' } })
// [4]
await db.find({ $includes: { 'properties.variants': 'strong' } })
// Error, field is not an array
await db.find({ $includes: { type: 'weak' } })
// Error, dot notation isn't a valid object field
await db.find({ $includes: { properties: { 'variants.0': 'weak' } } })

// $or
// [1, 2, 4]
await db.find({ $or: [{ type: 'weak' }, { type: 'normal' }] })
// [1, 2, 3, 4, 5]
await db.find({ $or: [{ $includes: { variants: 'weak' } }, { _id: 5 }] })

Projection

leaf-db supports projection. When using projection, only the specified fields will be returned. Empty objects can be returned if projection is [], or when none of the fields provided exist on the found objects.

Example

// Data
// { _id: 1, type: 'normal', important: false, variants: ['weak', 'strong'] }
// { _id: 2, type: 'normal', important: true, variants: ['weak', 'strong'] }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3, variants: ['strong'] } }
// { _id: 5, properties: [{ variants: ['weak', 'normal' ] }, { type: 'strong' }] }

// [{ _id: 1 }, { _id: 2 }]
await db.find({ type: 'normal' }, ['_id'])

// [{ type: 'normal' }, { type: 'normal' }, { type: 'strong' }, { type: 'weak' }, {}]
await db.find({}, ['type'])

Indexing

leaf-db uses a hash table under the hood to store docs. All docs are indexed by _id, meaning using any byId query is considerably faster than its regular counterpart.

The byId queries accept a single _id string, or an array of _id strings.

Updating docs

await db.update(Query | string[], Update | NewDoc) => Promise<Doc[]>

await db.updateById(string, Update) => Promise<Doc>

Find doc(s) matching query object. update() supports modifiers, but fields and modifiers cannot be mixed together. update cannot create invalid field names, such as fields containing dots or fields starting with $. Returns the updated docs.

If no modifiers are provided, update() will override the found doc(s) with update

_id fields cannot be overwritten. Trying to do so will throw an error.

Example

// Data
// { _id: 1, type: 'normal', important: false, variants: ['weak', 'strong'] }
// { _id: 2, type: 'normal', important: true, variants: ['weak', 'strong'] }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3, variants: ['strong'] } }

// Set all docs to {}
await db.update()

// Set matching docs to { type: 'strong' }
// { _id: 1, type: 'strong' }
// { _id: 2, type: 'strong' }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3, variants: ['strong'] } }
await db.update({ type: 'normal' }, { type: 'strong' })

// _id fields will not be overwritten
// { _id: 1, type: 'strong' }
// { _id: 2, type: 'strong' }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3, variants: ['strong'] } }
await db.update({ type: 'normal' }, { type: 'strong', _id: 1 })

// Error, dot notation isn't a valid field
await db.update({ type: 'normal' }, { 'properties.type': 'strong', _id: 1 })

Modifiers

Modifiers can be used to set specific values

  • $add - Add value (number)
  • $push - Add value (array)
  • $set - Set value

Example

// Data
// { _id: 1 }
// { _id: 2 }
// { _id: 3, count: 3 }

// $add
// { _id: 3, count: 9 }
await db.update({} }, { $add: { count: 3 } })
// { _id: 3, count: 3 }
await db.update({}, { $add: { count: -3 } })

// $push
// { _id: 3, fruits: ['banana'] }
await db.update({} }, { $push: { count: 'orange' } })
// { _id: 3 , fuits: ['banana', 'orange'] }

// $set
// { _id: 3, count: 'count' }
await db.update({ $keys: ['count'] }, { $set: { count: 'count' } })
// { _id: 1, value: 3 }
// { _id: 2, value: 3 }
// { _id: 3, count: 3, value: 3 }
// Keys will be created if it does not exist
await db.update({}, { $set: { value: 3 } })

Deleting docs

await db.delete(Query | string[]) => Promise<number>

await db.deleteById(string) => Promise<number>

Delete doc(s) matching query object.

Example

// Data in database
// { _id: 1, type: 'normal', important: false, variants: ['weak', 'strong'] }
// { _id: 2, type: 'normal', important: true, variants: ['weak', 'strong'] }
// { _id: 3, type: 'strong', important: false, variants: ['weak', 'strong'] }
// { _id: 4, type: 'weak', variants: ['weak'], properties: { type: 'weak', parent: 3, variants: ['strong'] } }

// Delete all data
// []
await db.delete()

// Delete first match
// [1, 3, 4]
await db.delete({ _id: 2 })

// Delete all matches
// [3, 4]
await db.delete({ type: 'normal' })

Drop

drop() => void

Clears both memory and database file.

Donating

ko-fi

Acknowledgements