const EventEmitter = require('events'); const OriginalDatastore = require('@seald-io/nedb'); const Cursor = require('./Cursor'); /** * @summary * As of v2.0.0 the Datastore class extends node's built * in EventEmitter class and implements each method as an event * plus additional error events. It also inherits the `compaction.done` * event from nedb but for consistency, in this library the event * was renamed to `compactionDone`. * * All event callbacks will be passed the same type of values, * the first being the datastore, then the operation result (if there is any) * and then the arguments of the called method. (Check out the first example!) * * All events have a matching error event that goes by the name of `${method}Error`, * for example `findError` or `loadError`. The callbacks of these events will receive * the same parameters as the normal event handlers except that instead of the * operation result there will be an operation error. (Check out the second example!) * * A generic `__error__` event is also available. This event will be emitted at any of * the above error events. The callbacks of this event will receive the same parameters * as the specific error event handlers except that there will be one more parameter * passed between the datastore and the error object, that being the name of the method * that failed. (Check out the third example!) * * @example * let datastore = Datastore.create() * datastore.on('update', (datastore, result, query, update, options) => { * }) * datastore.on('load', (datastore) => { * // this event doesn't have a result * }) * datastore.on('ensureIndex', (datastore, options) => { * // this event doesn't have a result * // but it has the options argument which will be passed to the * // event handlers * }) * datastore.on('compactionDone', (datastore) => { * // inherited from nedb's compaction.done event * }) * * @example * let datastore = Datastore.create() * datastore.on('updateError', (datastore, error, query, update, options) => { * }) * datastore.on('loadError', (datastore, error) => { * }) * datastore.on('ensureIndexError', (datastore, error, options) => { * }) * * @example * let datastore = Datastore.create() * datastore.on('__error__', (datastore, event, error, ...args) => { * // for example * // datastore, 'find', error, [{ foo: 'bar' }, {}] * }) * * @class */ class Datastore extends EventEmitter { /** * Create a database instance. * * Use this over `new Datastore(...)` to access * original nedb datastore properties, such as * `datastore.persistence`. * * Note that this method only creates the `Datastore` * class instance, not the datastore file itself. * The file will only be created once an operation * is issued against the datastore or if you call * the `load` instance method explicitly. * * The path (if specified) will be relative to `process.cwd()` * (unless an absolute path was passed). * * For more information visit: * https://github.com/louischatriot/nedb#creatingloading-a-database * * @param {string|Object} [pathOrOptions] * @return {Proxy} */ static create(pathOrOptions) { return new Proxy(new this(pathOrOptions), { get(target, key) { return target[key] ? target[key] : target.__original[key]; }, set(target, key, value) { return Object.prototype.hasOwnProperty.call(target.__original, key) ? (target.__original[key] = value) : (target[key] = value); }, }); } /** * Datastore constructor... * * You should use `Datastore.create(...)` instead * of `new Datastore(...)`. With that you can access * the original datastore's properties such as `datastore.persistence`. * * Create a Datastore instance. * * Note that the datastore will be created * relative to `process.cwd()` * (unless an absolute path was passed). * * It's basically the same as the original: * https://github.com/louischatriot/nedb#creatingloading-a-database * * @param {string|Object} [pathOrOptions] * @return {static} */ constructor(pathOrOptions) { super(); const datastore = new OriginalDatastore( typeof pathOrOptions === 'string' ? { filename: pathOrOptions } : pathOrOptions, ); Object.defineProperties(this, { __loaded: { enumerable: false, writable: true, value: null, }, __original: { configurable: true, enumerable: false, writable: false, value: datastore, }, }); this.__original.on('compaction.done', () => { this.emit('compactionDone', this); }); } /** * Load the datastore. * * Note that you don't necessarily have to call * this method to load the datastore as it will * automatically be called and awaited on any * operation issued against the datastore * (i.e.: `find`, `findOne`, etc.). * * @return {Promise} */ load() { if ( ! (this.__loaded instanceof Promise)) { this.__loaded = this.__original.loadDatabaseAsync() .then(() => this.broadcastSuccess('load')) .catch((error) => { this.broadcastError('load', error); throw error; }); } return this.__loaded; } /** * Find documents that match the specified `query`. * * It's basically the same as the original: * https://github.com/louischatriot/nedb#finding-documents * * There are differences minor in how the cursor works though. * * @example * datastore.find({ ... }).sort({ ... }).exec().then(...) * * @example * datastore.find({ ... }).sort({ ... }).then(...) * * @example * // in an async function * await datastore.find({ ... }).sort({ ... }) * * @param {Object} [query] * @param {Object} [projection] * @return {Cursor} */ find(query = {}, projection) { if (typeof projection === 'function') { projection = {}; } return new Cursor(this, 'find', query, projection); } /** * Find a document that matches the specified `query`. * * It's basically the same as the original: * https://github.com/louischatriot/nedb#finding-documents * * @example * datastore.findOne({ ... }).then(...) * * @example * // in an async function * await datastore.findOne({ ... }).sort({ ... }) * * @param {Object} [query] * @param {Object} [projection] * @return {Cursor} */ findOne(query = {}, projection) { if (typeof projection === 'function') { projection = {}; } return new Cursor(this, 'findOne', query, projection); } /** * Insert a document or documents. * * It's basically the same as the original: * https://github.com/louischatriot/nedb#inserting-documents * * @param {Object|Object[]} docs * @return {Promise} */ async insert(docs) { await this.load(); try { const result = await this.__original.insertAsync(docs); this.broadcastSuccess('insert', docs); return result; } catch (error) { this.broadcastError('insert', error, docs); throw error; } } /** * Insert a single document. * * This is just an alias for `insert` with object destructuring * to ensure a single document. * * @param {Object} doc * @return {Promise} */ insertOne({ ...doc }) { return this.insert(doc); } /** * Insert multiple documents. * * This is just an alias for `insert` with array destructuring * to ensure multiple documents. * * @param {Object[]} docs * @return {Promise} */ insertMany([...docs]) { return this.insert(docs); } /** * Update documents that match the specified `query`. * * It's basically the same as the original: * https://github.com/louischatriot/nedb#updating-documents * * If you set `options.returnUpdatedDocs`, * the returned promise will resolve with * an object (if `options.multi` is `false`) or * with an array of objects. * * @param {Object} query * @param {Object} update * @param {Object} [options] * @return {Promise} */ async update(query, update, options = {}) { await this.load(); try { const { numAffected, affectedDocuments } = await this.__original.updateAsync(query, update, options); const result = options.returnUpdatedDocs ? affectedDocuments : numAffected; this.broadcastSuccess('update', result, query, update, options); return result; } catch (error) { this.broadcastError('update', error, query, update, options); throw error; } } /** * Update a single document that matches the specified `query`. * * This is just an alias for `update` with `options.multi` set to `false`. * * @param {Object} query * @param {Object} update * @param {Object} [options] * * @return {Promise} */ updateOne(query, update, options = {}) { return this.update(query, update, { ...options, multi: false }); } /** * Update multiple documents that match the specified `query`. * * This is just an alias for `update` with `options.multi` set to `true`. * * @param {Object} query * @param {Object} update * @param {Object} [options] * * @return {Promise} */ updateMany(query, update, options = {}) { return this.update(query, update, { ...options, multi: true }); } /** * Remove documents that match the specified `query`. * * It's basically the same as the original: * https://github.com/louischatriot/nedb#removing-documents * * @param {Object} [query] * @param {Object} [options] * @return {Promise} */ async remove(query = {}, options = {}) { await this.load(); try { const result = await this.__original.removeAsync(query, options); this.broadcastSuccess('remove', result, query, options); return result; } catch (error) { this.broadcastError('remove', error, query, options); throw error; } } /** * Remove the first document that matches the specified `query`. * * This is just an alias for `remove` with `options.multi` set to `false`. * * @param {Object} [query] * @param {Object} [options] * * @return {Promise} */ removeOne(query, options = {}) { return this.remove(query, { ...options, multi: false }); } /** * Remove all documents that match the specified `query`. * * This is just an alias for `remove` with `options.multi` set to `true`. * * @param {Object} [query] * @param {Object} [options] * * @return {Promise} */ removeMany(query, options = {}) { return this.remove(query, { ...options, multi: true }); } /** * Remove the first document that matches the specified `query`. * * This is just an alias for `removeOne`. * * @param {Object} [query] * @param {Object} [options] * * @return {Promise} */ deleteOne(query, options) { return this.removeOne(query, options); } /** * Remove all documents that match the specified `query`. * * This is just an alias for `removeMany`. * * @param {Object} [query] * @param {Object} [options] * * @return {Promise} */ deleteMany(query, options) { return this.removeMany(query, options); } /** * Count documents matching the specified `query`. * * It's basically the same as the original: * https://github.com/louischatriot/nedb#counting-documents * * @example * datastore.count({ ... }).limit(...).then(...) * * @example * // in an async function * await datastore.count({ ... }) * // or * await datastore.count({ ... }).sort(...).limit(...) * * @param {Object} [query] * @return {Cursor} */ count(query = {}) { return new Cursor(this, 'count', query); } /** * https://github.com/louischatriot/nedb#indexing * * @param {Object} options * @return {Promise} */ async ensureIndex(options) { try { const result = await this.__original.ensureIndexAsync(options); this.broadcastSuccess('ensureIndex', result, options); return result; } catch (error) { this.broadcastError('ensureIndex', error, options); throw error; } } /** * https://github.com/louischatriot/nedb#indexing * * @param {string} field * @return {Promise} */ async removeIndex(field) { try { const result = await this.__original.removeIndexAsync(field); this.broadcastSuccess('removeIndex', result, field); return result; } catch (error) { this.broadcastError('removeIndex', error, field); throw error; } } /** * Broadcasts operation success messages. * * @param {string} op * @param {*} result * @param {...*} args * * @return {undefined} * @private */ broadcastSuccess(op, result, ...args) { this.emit(op, this, result, ...args); return this; } /** * Broadcasts operation error messages. * * @param {string} op * @param {Error} error * @param {...*} args * * @return {undefined} * @private */ broadcastError(op, error, ...args) { this.emit(`${op}Error`, this, error, ...args); this.emit('__error__', this, op, error, ...args); return this; } } module.exports = Datastore;