diff --git a/README.md b/README.md index 4acf51e..be5b576 100755 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ 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 [`docs`](./docs) directory of the repository. +documentation is available in the [`docs`](./API.md) directory of the repository. ### Creating/loading a database @@ -87,20 +87,38 @@ await db.users.loadDatabaseAsync() await db.robots.loadDatabaseAsync() ``` +### Dropping a database + +Since v3.0.0, you can drop the database by using [`Datastore#dropDatabaseAsync`](./API.md#Datastore+dropDatabaseAsync): +```js +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](./docs/Persistence.md) uses an append-only +Under the hood, NeDB's [persistence](./API.md#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#persistence#compactDatafileAsync`](./API.md#Persistence+compactDatafileAsync). +with [`yourDatabase#compactDatafileAsync`](./API.md#Datastore+compactDatafileAsync). You can also set automatic compaction at regular intervals -with [`yourDatabase#persistence#setAutocompactionInterval`](./API.md#Persistence+setAutocompactionInterval), -and stop automatic compaction with [`yourDatabase#persistence#stopAutocompaction`](./API.md#Persistence+stopAutocompaction). +with [`yourDatabase#setAutocompactionInterval`](./API.md#Datastore+setAutocompactionInterval), +and stop automatic compaction with [`yourDatabase#stopAutocompaction`](./API.md#Datastore+stopAutocompaction). ### Inserting documents @@ -385,7 +403,7 @@ const docs = await db.findAsync({ [`Datastore#findAsync`](./API.md#Datastore+findAsync), [`Datastore#findOneAsync`](./API.md#Datastore+findOneAsync) and [`Datastore#countAsync`](./API.md#Datastore+countAsync) don't -actually return a `Promise`, but a [`Cursor`](./docs/Cursor.md) which is a +actually return a `Promise`, but a [`Cursor`](./API.md#Cursor) which is a [`Thenable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#thenable_objects) which calls [`Cursor#execAsync`](./API.md#Cursor+execAsync) when awaited. @@ -783,16 +801,16 @@ for the `browser` field. And this is [done by default by Metro](https://github.c for the `react-native` field. This is done for: -- the [storage module](./docs/storage.md) which uses Node.js `fs`. It is - [replaced in the browser](./docs/storageBrowser.md) by one that uses +- the [storage module](./lib/storage.js) which uses Node.js `fs`. It is + [replaced in the browser](./browser-version/lib/storage.browser.js) by one that uses [localforage](https://github.com/localForage/localForage), and - [in `react-native`](./docs/storageBrowser.md) by one that uses + [in `react-native`](./browser-version/lib/storage.react-native.js) by one that uses [@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage) -- the [customUtils module](./docs/customUtilsNode.md) which uses Node.js +- the [customUtils module](./browser-version/lib/customUtils.js) which uses Node.js `crypto` module. It is replaced by a good enough shim to generate ids that uses `Math.random()`. -- the [byline module](./docs/byline.md) which uses Node.js `stream` +- the [byline module](./browser-version/lib/byline.js) which uses Node.js `stream` (a fork of [`node-byline`](https://github.com/jahewson/node-byline) included in - the repo because it is unmaintained). It isn't used int the browser nor + the repo because it is unmaintained). It isn't used in the browser nor react-native versions, therefore it is shimmed with an empty object. ## Performance @@ -849,7 +867,7 @@ to make it manageable: 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 files](./docs). +* Update the JSDoc and regenerate [the markdown docs](./API.md). * Update the readme accordingly. ## License diff --git a/index.d.ts b/index.d.ts index ae85d47..7cba2bb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,6 +24,18 @@ declare class Nedb extends EventEmitter { loadDatabaseAsync(): Promise; + dropDatabase(callback?: (err: Error |null) => void): void; + + dropDatabaseAsync(): Promise; + + compactDatafile(callback?: (err: Error |null) => void): void; + + compactDatafileAsync(): Promise; + + setAutocompactionInterval(interval: number): void; + + stopAutocompaction(): void; + getAllData(): T[]; ensureIndex(options: Nedb.EnsureIndexOptions, callback?: (err: Error | null) => void): void; @@ -101,8 +113,6 @@ declare namespace Nedb { afterSerialization?(line: string): string; corruptAlertThreshold?: number; compareStrings?(a: string, b: string): number; - /** @deprecated */ - nodeWebkitAppName?: string; } interface UpdateOptions { @@ -123,9 +133,13 @@ declare namespace Nedb { } interface Persistence { + /** @deprecated */ compactDatafile(): void; + /** @deprecated */ compactDatafileAsync(): Promise; + /** @deprecated */ setAutocompactionInterval(interval: number): void; + /** @deprecated */ stopAutocompaction(): void; } } diff --git a/test/persistence.test.js b/test/persistence.test.js index d0f11fb..774af46 100755 --- a/test/persistence.test.js +++ b/test/persistence.test.js @@ -10,6 +10,8 @@ const Persistence = require('../lib/persistence') const storage = require('../lib/storage') const { execFile, fork } = require('child_process') const { callbackify } = require('util') +const { existsCallback } = require('./utils.test') +const { ensureFileDoesntExistAsync } = require('../lib/storage') const Readable = require('stream').Readable const { assert } = chai @@ -1054,4 +1056,162 @@ describe('Persistence', function () { }) }) }) // ==== End of 'Prevent dataloss when persisting data' ==== + + describe('dropDatabase', function () { + it('deletes data in memory', done => { + const inMemoryDB = new Datastore({ inMemoryOnly: true }) + inMemoryDB.insert({ hello: 'world' }, err => { + assert.equal(err, null) + inMemoryDB.dropDatabase(err => { + assert.equal(err, null) + assert.equal(inMemoryDB.getAllData().length, 0) + return done() + }) + }) + }) + + it('deletes data in memory & on disk', done => { + d.insert({ hello: 'world' }, err => { + if (err) return done(err) + d.dropDatabase(err => { + if (err) return done(err) + assert.equal(d.getAllData().length, 0) + existsCallback(testDb, bool => { + assert.equal(bool, false) + done() + }) + }) + }) + }) + + it('check that executor is drained before drop', done => { + for (let i = 0; i < 100; i++) { + d.insert({ hello: 'world' }) // no await + } + d.dropDatabase(err => { // it should await the end of the inserts + if (err) return done(err) + assert.equal(d.getAllData().length, 0) + existsCallback(testDb, bool => { + assert.equal(bool, false) + done() + }) + }) + }) + + it('check that autocompaction is stopped', done => { + d.setAutocompactionInterval(5000) + d.insert({ hello: 'world' }, err => { + if (err) return done(err) + d.dropDatabase(err => { + if (err) return done(err) + assert.equal(d.autocompactionIntervalId, null) + assert.equal(d.getAllData().length, 0) + existsCallback(testDb, bool => { + assert.equal(bool, false) + done() + }) + }) + }) + }) + + it('check that we can reload and insert afterwards', done => { + d.insert({ hello: 'world' }, err => { + if (err) return done(err) + d.dropDatabase(err => { + if (err) return done(err) + assert.equal(d.getAllData().length, 0) + existsCallback(testDb, bool => { + assert.equal(bool, false) + d.loadDatabase(err => { + if (err) return done(err) + d.insert({ hello: 'world' }, err => { + if (err) return done(err) + assert.equal(d.getAllData().length, 1) + d.compactDatafile(err => { + if (err) return done(err) + existsCallback(testDb, bool => { + assert.equal(bool, true) + done() + }) + }) + }) + }) + }) + }) + }) + }) + + it('check that we can dropDatatabase if the file is already deleted', done => { + callbackify(ensureFileDoesntExistAsync)(testDb, err => { + if (err) return done(err) + existsCallback(testDb, bool => { + assert.equal(bool, false) + d.dropDatabase(err => { + if (err) return done(err) + existsCallback(testDb, bool => { + assert.equal(bool, false) + done() + }) + }) + }) + }) + }) + + it('Check that TTL indexes are reset', done => { + d.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 10 }) + const date = new Date() + d.insert({ hello: 'world', expire: new Date(date.getTime() - 1000 * 20) }, err => { // expired by 10 seconds + if (err) return done(err) + d.find({}, (err, docs) => { + if (err) return done(err) + assert.equal(docs.length, 0) // the TTL makes it so that the document is not returned + d.dropDatabase(err => { + if (err) return done(err) + assert.equal(d.getAllData().length, 0) + existsCallback(testDb, bool => { + assert.equal(bool, false) + d.loadDatabase(err => { + if (err) return done(err) + d.insert({ hello: 'world', expire: new Date(date.getTime() - 1000 * 20) }, err => { + if (err) return done(err) + d.find({}, (err, docs) => { + if (err) return done(err) + assert.equal(docs.length, 1) // the TTL makes it so that the document is not returned + d.compactDatafile(err => { + if (err) return done(err) + existsCallback(testDb, bool => { + assert.equal(bool, true) + done() + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) + + it('Check that the buffer is reset', done => { + d.dropDatabase(err => { + if (err) return done(err) + // these 3 will hang until load + d.insert({ hello: 'world' }) + d.insert({ hello: 'world' }) + d.insert({ hello: 'world' }) + assert.equal(d.getAllData().length, 0) + d.dropDatabase(err => { + if (err) return done(err) + d.insert({ hi: 'world' }) + d.loadDatabase(err => { + if (err) return done(err) + assert.equal(d.getAllData().length, 1) + assert.equal(d.getAllData()[0].hi, 'world') + done() + }) + }) + }) + }) + }) // ==== End of 'dropDatabase' ==== }) diff --git a/test/utils.test.js b/test/utils.test.js index d7272ee..214c31f 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -33,10 +33,14 @@ const wait = delay => new Promise(resolve => { }) const exists = path => fs.access(path, fsConstants.FS_OK).then(() => true, () => false) +// eslint-disable-next-line node/no-callback-literal +const existsCallback = (path, callback) => fs.access(path, fsConstants.FS_OK).then(() => callback(true), () => callback(false)) + module.exports.whilst = whilst module.exports.apply = apply module.exports.waterfall = waterfall module.exports.each = each module.exports.wait = wait module.exports.exists = exists +module.exports.existsCallback = existsCallback module.exports.callbackify = callbackify