The JavaScript Database
This module is a fork of nedb
written by Louis Chatriot.
Since the original maintainer doesn't support this package anymore, we forked it
and maintain it for the needs of Seald.
Embedded persistent or in memory database for Node.js, Electron and browsers,
100% JavaScript, no binary dependency. API is a subset of MongoDB's and it's
plenty fast.
Installation
Module name on npm is @seald-io/nedb
.
npm install @seald-io/nedb
Then to import, you just have to:
const Datastore = require('@seald-io/nedb')
Documentation
The API is a subset of MongoDB's API (the most used operations).
Since version 3.0.0, NeDB provides a Promise-based equivalent for each function
which is suffixed with Async
, for example loadDatabaseAsync
.
The original callback-based interface is still available, fully retro-compatible
(as far as the test suites can tell) and are a shim to this Promise-based
version.
Don't hesitate to open an issue if it breaks something in your project.
The rest of the readme will only show the Promise-based API, the full
documentation is available in the API.md
file at the root of the
repository. It is generated by running npm run generateDocs:markdown
.
- Creating/loading a database
- Dropping a database
- Persistence
- Inserting documents
- Finding documents
- Basic Querying
- Operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $exists, $regex)
- Array fields
- Logical operators $or, $and, $not, $where
- Sorting and paginating
- Projections
- Counting documents
- Updating documents
- Removing documents
- Indexing
- Other environments
Creating/loading a database
You can use NeDB as an in-memory only datastore or as a persistent datastore.
One datastore is the equivalent of a MongoDB collection. The constructor is used
as follows new Datastore(options)
where options
is an object.
If the Datastore is persistent (if you give it options.filename
,
you'll need to load the database using Datastore#loadDatabaseAsync,
or using options.autoload
.
const Datastore = require('@seald-io/nedb')
const db = new Datastore()
const Datastore = require('@seald-io/nedb')
const db = new Datastore({ filename: 'path/to/datafile' })
try {
await db.loadDatabaseAsync()
} catch (error) {
}
const Datastore = require('@seald-io/nedb')
const db = new Datastore({ filename: 'path/to/datafile', autoload: true })
db = {}
db.users = new Datastore('path/to/users.db')
db.robots = new Datastore('path/to/robots.db')
await db.users.loadDatabaseAsync()
await db.robots.loadDatabaseAsync()
Dropping a database
Since v3.0.0, you can drop the database by using Datastore#dropDatabaseAsync
:
const Datastore = require('@seald-io/nedb')
const db = new Datastore()
await d.insertAsync({ hello: 'world' })
await d.dropDatabaseAsync()
assert.equal(d.getAllData().length, 0)
assert.equal(await exists(testDb), false)
It is not recommended to keep using an instance of Datastore when its database
has been dropped as it may have some unintended side effects.
Persistence
Under the hood, NeDB's persistence uses an append-only
format, meaning that all updates and deletes actually result in lines added at
the end of the datafile, for performance reasons. The database is automatically
compacted (i.e. put back in the one-line-per-document format) every time you
load each database within your application.
Breaking change: since v3.0.0, calling methods of yourDatabase.persistence
is deprecated. The same functions exists directly on the Datastore
.
You can manually call the compaction function
with yourDatabase#compactDatafileAsync
.
You can also set automatic compaction at regular intervals
with yourDatabase#setAutocompactionInterval
,
and stop automatic compaction with yourDatabase#stopAutocompaction
.
Inserting documents
The native types are String
, Number
, Boolean
, Date
and null
. You can
also use arrays and subdocuments (objects). If a field is undefined
, it will
not be saved (this is different from MongoDB which transforms undefined
in null
, something I find counter-intuitive).
If the document does not contain an _id
field, NeDB will automatically
generate one for you (a 16-characters alphanumerical string). The _id
of a
document, once set, cannot be modified.
Field names cannot start with '$' or contain the characters '.' and ','.
const doc = {
hello: 'world',
n: 5,
today: new Date(),
nedbIsAwesome: true,
notthere: null,
notToBeSaved: undefined,
fruits: ['apple', 'orange', 'pear'],
infos: { name: '@seald-io/nedb' }
}
try {
const newDoc = await db.insertAsync(doc)
} catch (error) {
}
You can also bulk-insert an array of documents. This operation is atomic,
meaning that if one insert fails due to a unique constraint being violated, all
changes are rolled back.
const newDocs = await db.insertAsync([{ a: 5 }, { a: 42 }])
try {
await db.insertAsync([{ a: 5 }, { a: 42 }, { a: 5 }])
} catch (error) {
}
Finding documents
Use findAsync
to look for multiple documents matching you query, or findOneAsync
to
look for one specific document. You can select documents based on field equality
or use comparison operators ($lt
, $lte
, $gt
, $gte
, $in
, $nin
, $ne
)
. You can also use logical operators $or
, $and
, $not
and $where
. See
below for the syntax.
You can use regular expressions in two ways: in basic querying in place of a
string, or with the $regex
operator.
You can sort and paginate results using the cursor API (see below).
You can use standard projections to restrict the fields to appear in the
results (see below).
Basic querying
Basic querying means are looking for documents whose fields match the ones you
specify. You can use regular expression to match strings. You can use the dot
notation to navigate inside nested documents, arrays, arrays of subdocuments and
to match a specific element of an array.
const docs = await db.findAsync({ system: 'solar' })
const docs = await db.findAsync({ planet: /ar/ })
const docs = await db.findAsync({ system: 'solar', inhabited: true })
const docs = await db.findAsync({ 'humans.genders': 2 })
const docs = await db.findAsync({ 'completeData.planets.name': 'Mars' })
const docs = await db.findAsync({ 'completeData.planets.name': 'Jupiter' })
const docs = await db.findAsync({ 'completeData.planets.0.name': 'Earth' })
const docs = await db.findAsync({ humans: { genders: 2 } })
const docs = await db.findAsync({})
const doc = await db.findOneAsync({ _id: 'id1' })
Operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $exists, $regex)
The syntax is { field: { $op: value } }
where $op
is any comparison
operator:
$lt
, $lte
: less than, less than or equal$gt
, $gte
: greater than, greater than or equal$in
: member of. value
must be an array of values$ne
, $nin
: not equal, not a member of$exists
: checks whether the document posses the property field
. value
should be true or false$regex
: checks whether a string is matched by the regular expression.
Contrary to MongoDB, the use of $options
with $regex
is not supported,
because it doesn't give you more power than regex flags. Basic queries are
more readable so only use the $regex
operator when you need to use another
operator with it (see example below)
const docs = await db.findAsync({ 'humans.genders': { $gt: 5 } })
const docs = await db.findAsync({ planet: { $gt: 'Mercury' } })
const docs = await db.findAsync({ planet: { $in: ['Earth', 'Jupiter'] } })
const docs = await db.findAsync({ satellites: { $exists: true } })
const docs = await db.findAsync({
planet: {
$regex: /ar/,
$nin: ['Jupiter', 'Earth']
}
})
Array fields
When a field in a document is an array, NeDB first tries to see if the query
value is an array to perform an exact match, then whether there is an
array-specific comparison function (for now there is only $size
and $elemMatch
) being used. If not, the query is treated as a query on every
element and there is a match if at least one element matches.
$size
: match on the size of the array$elemMatch
: matches if at least one array element matches the query entirely
const docs = await db.findAsync({ satellites: ['Phobos', 'Deimos'] })
const docs = await db.findAsync({ satellites: ['Deimos', 'Phobos'] })
const docs = await db.findAsync({
completeData: {
planets: {
$elemMatch: {
name: 'Earth',
number: 3
}
}
}
})
const docs = await db.findAsync({
completeData: {
planets: {
$elemMatch: {
name: 'Earth',
number: 5
}
}
}
})
const docs = await db.findAsync({
completeData: {
planets: {
$elemMatch: {
name: 'Earth',
number: { $gt: 2 }
}
}
}
})
const docs = await db.findAsync({ satellites: { $size: 2 } })
const docs = await db.findAsync({ satellites: { $size: 1 } })
const docs = await db.findAsync({ satellites: 'Phobos' })
const docs = await db.findAsync({ satellites: { $lt: 'Amos' } })
const docs = await db.findAsync({ satellites: { $in: ['Moon', 'Deimos'] } })
Logical operators $or, $and, $not, $where
You can combine queries using logical operators:
- For
$or
and $and
, the syntax is { $op: [query1, query2, ...] }
. - For
$not
, the syntax is { $not: query }
- For
$where
, the syntax
is { $where: function () { /* object is 'this', return a boolean */ } }
const docs = await db.findAsync({ $or: [{ planet: 'Earth' }, { planet: 'Mars' }] })
const docs = await db.findAsync({ $not: { planet: 'Earth' } })
const docs = await db.findAsync({ $where: function () { return Object.keys(this) > 6 } })
const docs = await db.findAsync({
$or: [{ planet: 'Earth' }, { planet: 'Mars' }],
inhabited: true
})
Sorting and paginating
Datastore#findAsync
,
Datastore#findOneAsync
and
Datastore#countAsync
don't
actually return a Promise
, but a Cursor
which is a
Thenable
which calls Cursor#execAsync
when awaited.
This pattern allows to chain Cursor#sort
,
Cursor#skip
,
Cursor#limit
and
Cursor#projection
and await the result.
const docs = await db.findAsync({}).sort({ planet: 1 }).skip(1).limit(2)
const docs = await db.findAsync({ system: 'solar' }).sort({ planet: -1 })
const docs = await db.findAsync({}).sort({ firstField: 1, secondField: -1 })
Projections
You can give findAsync
and findOneAsync
an optional second argument, projections
.
The syntax is the same as MongoDB: { a: 1, b: 1 }
to return only the a
and b
fields, { a: 0, b: 0 }
to omit these two fields. You cannot use both
modes at the time, except for _id
which is by default always returned and
which you can choose to omit. You can project on nested documents.
const docs = await db.findAsync({ planet: 'Mars' }, { planet: 1, system: 1 })
const docs = await db.findAsync({ planet: 'Mars' }, {
planet: 1,
system: 1,
_id: 0
})
const docs = await db.findAsync({ planet: 'Mars' }, {
planet: 0,
system: 0,
_id: 0
})
const docs = await db.findAsync({ planet: 'Mars' }, { planet: 0, system: 1 })
const docs = await db.findAsync({ planet: 'Mars' }).projection({
planet: 1,
system: 1
})
const doc = await db.findOneAsync({ planet: 'Earth' }).projection({
planet: 1,
'humans.genders': 1
})
Counting documents
You can use countAsync
to count documents. It has the same syntax as findAsync
.
For example:
const count = await db.countAsync({ system: 'solar' })
const count = await db.countAsync({})
Updating documents
db.updateAsync(query, update, options)
will update all documents matching query
according to the update
rules.
update
specifies how the documents should be modified. It is either a new
document or a set of modifiers (you cannot use both together):
- A new document will replace the matched docs;
- Modifiers create the fields they need to modify if they don't exist,
and you can apply them to subdocs (see the API reference)
options
is an object with three possible parameters:
multi
which allows the modification of several documents if set to true.upsert
will insert a new document corresponding if it doesn't exist (either
the update
is a simple object with no modifiers, or the query
modified by
the modifiers in the update
) if set to true
.returnUpdatedDocs
will return the array of documents matched by the find
query and updated (updated documents will be returned even if the update did not
actually modify them) if set to true
.
It resolves into an Object with the following properties:
numAffected
: how many documents were affected by the update;upsert
: if a document was actually upserted (not always the same as options.upsert
;affectedDocuments
:
- if
upsert
is true
the document upserted; - if
options.returnUpdatedDocs
is true
either the affected document or, if options.multi
is true
an Array of the affected documents, else null
;
Note: you can't change a document's _id.
const { numReplaced } = await db.updateAsync({ planet: 'Jupiter' }, { planet: 'Pluton' }, {})
const { numReplaced } = await db.updateAsync({ system: 'solar' }, { $set: { system: 'solar system' } }, { multi: true })
await db.updateAsync({ planet: 'Mars' }, {
$set: {
'data.satellites': 2,
'data.red': true
}
}, {})
await db.updateAsync({ planet: 'Mars' }, { $set: { data: { satellites: 3 } } }, {})
await db.updateAsync({ planet: 'Mars' }, { $unset: { planet: true } }, {})
const { numReplaced, affectedDocuments, upsert } = await db.updateAsync({ planet: 'Pluton' }, {
planet: 'Pluton',
inhabited: false
}, { upsert: true })
await db.updateAsync({ planet: 'Pluton' }, { $inc: { distance: 38 } }, { upsert: true })
await db.updateAsync({ _id: 'id6' }, { $push: { fruits: 'banana' } }, {})
await db.updateAsync({ _id: 'id6' }, { $pop: { fruits: 1 } }, {})
await db.updateAsync({ _id: 'id6' }, { $addToSet: { fruits: 'apple' } }, {})
await db.updateAsync({ _id: 'id6' }, { $pull: { fruits: 'apple' } }, {})
await db.updateAsync({ _id: 'id6' }, { $pull: { fruits: { $in: ['apple', 'pear'] } } }, {})
await db.updateAsync({ _id: 'id6' }, { $push: { fruits: { $each: ['banana', 'orange'] } } }, {})
await db.updateAsync({ _id: 'id6' }, {
$push: {
fruits: {
$each: ['banana'],
$slice: 2
}
}
})
await db.updateAsync({ _id: 'id1' }, { $min: { value: 2 } }, {})
await db.updateAsync({ _id: 'id1' }, { $min: { value: 8 } }, {})
Removing documents
db.removeAsync(query, options)
will remove documents matching query
. Can remove multiple documents if
options.multi
is set. Returns the Promise<numRemoved>
.
const { numRemoved } = await db.removeAsync({ _id: 'id2' }, {})
const { numRemoved } = await db.removeAsync({ system: 'solar' }, { multi: true })
const { numRemoved } = await db.removeAsync({}, { multi: true })
Indexing
NeDB supports indexing. It gives a very nice speed boost and can be used to
enforce a unique constraint on a field. You can index any field, including
fields in nested documents using the dot notation. For now, indexes are only
used to speed up basic queries and queries using $in
, $lt
, $lte
, $gt
and $gte
. The indexed values cannot be of type array of object.
Breaking change: since v4.0.0, commas (,
) can no longer be used in indexed field names.
The following is illegal:
db.ensureIndexAsync({ fieldName: 'some,field' })
db.ensureIndexAsync({ fieldName: ['some,field', 'other,field'] })
This is a side effect of the compound index implementation.
To create an index, use datastore#ensureIndexAsync(options)
.
It resolves when the index is persisted on disk (if the database is persistent)
and may throw an Error (usually a unique constraint that was violated). It can
be called when you want, even after some data was inserted, though it's best to
call it at application startup. The options are:
- fieldName (required): name of the field to index. Use the dot notation to
index a field in a nested document. For a compound index, use an array of field names.
- unique (optional, defaults to
false
): enforce field uniqueness. - sparse (optional, defaults to
false
): don't index documents for which
the field is not defined. - expireAfterSeconds (number of seconds, optional): if set, the created
index is a TTL (time to live) index, that will automatically remove documents
when the indexed field value is older than
expireAfterSeconds
.
Note: the _id
is automatically indexed with a unique constraint.
You can remove a previously created index with
datastore#removeIndexAsync(fieldName)
.
try {
await db.ensureIndexAsync({ fieldName: 'somefield' })
} catch (error) {
}
await db.ensureIndexAsync({ fieldName: 'somefield', unique: true })
await db.ensureIndexAsync({
fieldName: 'somefield',
unique: true,
sparse: true
})
await db.ensureIndexAsync({ fieldName: ["field1", "field2"] });
try {
await db.insertAsync({ somefield: '@seald-io/nedb' })
await db.insertAsync({ somefield: '@seald-io/nedb' })
} catch (error) {
}
await db.removeIndexAsync('somefield')
await db.ensureIndex({
fieldName: 'createdAt',
expireAfterSeconds: 3600
})
await db.ensureIndex({
fieldName: 'expirationDate',
expireAfterSeconds: 0
})
Other environments
NeDB runs on Node.js (it is tested on Node 12, 14 and 16), the browser (it is
tested on the latest version of Chrome) and React-Native using
@react-native-async-storage/async-storage.
Browser bundle
The npm package contains a bundle and its minified counterpart for the browser.
They are located in the browser-version/out
directory. You only need to require nedb.js
or nedb.min.js
in your HTML file and the global object Nedb
can be used
right away, with the same API as the server version:
<script src="nedb.min.js"></script>
<script>
var db = new Nedb(); // Create an in-memory only datastore
db.insert({ planet: 'Earth' }, function (err) {
db.find({}, function (err, docs) {
// docs contains the two planets Earth and Mars
});
});
</script>
If you specify a filename
, the database will be persistent, and automatically
select the best storage method available using localforage
(IndexedDB, WebSQL or localStorage) depending on the browser. In most cases that
means a lot of data can be stored, typically in hundreds of MB.
WARNING: the storage system changed between
v1.3 and v1.4 and is NOT back-compatible! Your application needs to resync
client-side when you upgrade NeDB.
NeDB uses modern Javascript features such as async
, Promise
, class
, const
, let
, Set
, Map
, ... The bundle does not polyfill these features. If you
need to target another environment, you will need to make your own bundle.
Using the browser
and react-native
fields
NeDB uses the browser
and react-native
fields to replace some modules by an
environment specific shim.
The way this works is by counting on the bundler that will package NeDB to use
this fields. This is done by default by Webpack
for the browser
field. And this is done by default by Metro
for the react-native
field.
This is done for:
Performance
Speed
NeDB is not intended to be a replacement of large-scale databases such as
MongoDB, and as such was not designed for speed. That said, it is still pretty
fast on the expected datasets, especially if you use indexing. On a typical,
not-so-fast dev machine, for a collection containing 10,000 documents, with
indexing:
- Insert: 10,680 ops/s
- Find: 43,290 ops/s
- Update: 8,000 ops/s
- Remove: 11,750 ops/s
You can run these simple benchmarks by executing the scripts in the benchmarks
folder. Run them with the --help
flag to see how they work.
A copy of the whole database is kept in memory. This is not much on the expected
kind of datasets (20MB for 10,000 2KB documents).
Use in other services
Modernization
This fork of NeDB will be incrementally updated to:
Pull requests guidelines
If you submit a pull request, thanks! There are a couple rules to follow though
to make it manageable:
- The pull request should be atomic, i.e. contain only one feature. If it
contains more, please submit multiple pull requests. Reviewing massive, 1000
loc+ pull requests is extremely hard.
- Likewise, if for one unique feature the pull request grows too large (more
than 200 loc tests not included), please get in touch first.
- Please stick to the current coding style. It's important that the code uses a
coherent style for readability (this package uses
standard
). - Do not include stylistic improvements ('housekeeping'). If you think one part
deserves lots of housekeeping, use a separate pull request so as not to
pollute the code.
- Don't forget tests for your new feature. Also don't forget to run the whole
test suite before submitting to make sure you didn't introduce regressions.
- Update the JSDoc and regenerate the markdown docs.
- Update the readme accordingly.
License
See License