moleculerjs/moleculer-db

View on GitHub
packages/moleculer-db-adapter-couchdb-nano/src/index.js

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * moleculer-db-adapter-couchdb-nano
 * Copyright (c) 2018 Mr. Kutin (https://github.com/moleculerjs/moleculer-db/tree/master/packages/moleculer-db-adapter-couchdb-nano#readme)
 * MIT Licensed
 */

"use strict";

const _ = require("lodash");
const Promise = require("bluebird");
const { ServiceSchemaError } = require("moleculer").Errors;
const Nano = require("nano");

class CouchDbNanoAdapter {

    /**
     * Creates an instance of CouchDbNano.
     * @param {String} url
     * @param {Object?} opts
     *
     * @memberof CouchDbNano
     */
    constructor(url, opts) {
        this.url = url;
        this.opts = opts;
        this.db = null;
    }

    /**
     * Initialize adapter
     *
     * @param {ServiceBroker} broker
     * @param {Service} service
     *
     * @memberof CouchDbNano
     */
    init(broker, service) {
        this.broker = broker;
        this.service = service;
        this.schema = service.schema.schema;
        this.modelName = (service.schema && service.schema.model && service.schema.model.name) || (service.schema && service.schema.modelName) || service.schema.name;
        if (!this.modelName) {
            throw new ServiceSchemaError("Missing `modelName` or `name` definition in schema of service!");
        }
    }

    /**
     * Connect to database
     *
     * @returns {Promise}
     *
     * @memberof CouchDbNano
     */
    connect() {
        const opts = Object.assign({}, this.opts, {url: this.url ? this.url.replace("couchdb://", "http://") : "" || "http://localhost:5984"});
        const nano = Nano(opts);
        this.db = nano.db;
        return this.db.get(this.modelName)
            .catch(e => {
                if (e.statusCode === 404) {
                    return this.db.create(this.modelName);
                }
                throw(e);
            })
            .then(() => {
                this.db = this.db.use(this.modelName);
                this.service.logger.info("CouchDB adapter has connected successfully.");
            });
    }

    /**
     * Disconnect from database
     *
     * @returns {Promise}
     *
     * @memberof CouchDbNano
     */
    disconnect() {
        this.db = null;
        return Promise.resolve();
    }

    /**
     * Insert an entity.
     *
     * @param {Object} entity
     * @returns {Promise<Object>} Return with the inserted document.
     *
     * @memberof CouchDbNano
     */
    insert(entity) {
        return Promise.resolve(
            this.db.insert(entity).then(result => this.findById(result.id))
        );
    }

    /**
     * Find an entity by ID.
     *
     * @param {String} _id
     * @returns {Promise<Object>} Return with the found document.
     *
     * @memberof CouchDbNano
     */
    findById(_id) {
        return Promise.resolve(this.db.get(_id));
    }

    /**
     * Remove an entity by ID
     *
     * @param {String} _id - ObjectID as hexadecimal string.
     * @returns {Promise<Object>} Return with the removed document.
     *
     * @memberof CouchDbNano
     */
    removeById(_id) {
        return Promise.resolve(
            this.findById(_id)
                .then(doc => {
                    return this.db.destroy(doc._id, doc._rev);
                })
                .then(res => {
                    res._id = res.id;
                    delete res.id;
                    return res;
                })
        );
    }

    /**
     * Find all entities by filters.
     *
     * Available filter props:
     *    - limit
     *  - offset
     *  - sort
     *  - search
     *  - searchFields
     *  - selector
     *
     * @param {Object} filters
     * @returns {Promise<Array>}
     *
     * @memberof CouchDbNano
     */
    find(filters) {
        const limit = filters.limit;
        const skip = filters.skip;
        const sort = filters.sort;
        const fields = filters.fields;

        const selector = filters.selector ? filters.selector : filters;
        if (selector.limit) {
            delete selector.limit;
        }
        if (selector.skip) {
            delete selector.skip;
        }
        if (selector.sort) {
            delete selector.sort;
        }
        if (selector.fields) {
            delete selector.fields;
        }

        Object.keys(selector).forEach(key => {
            if (Object.prototype.hasOwnProperty.call(selector, key) && typeof selector[key] !== "object") {
                selector[key] = {$eq: selector[key]};
            }
        });

        return Promise.resolve(this.db.find({selector, limit, skip, sort, fields}).then(result => result.docs));
    }

    /**
     * Find an entity by selector
     *
     * @param {Object} params
     * @returns {Promise}
     * @memberof MemoryDbAdapter
     */
    findOne(params) {
        const findParams = Object.assign({}, params.selector || params, {limit: 1});
        return this.find(findParams).then(docs => docs.length ? docs[0] : null);
    }

    /**
     * Find any entities by IDs.
     *
     * @param {Array} idList
     * @returns {Promise<Array>} Return with the found documents in an Array.
     *
     * @memberof CouchDbNano
     */
    findByIds(idList) {
        return Promise.resolve(
            this.db.fetch({keys: idList})
                .then(res => res.rows.map(el => el.doc))
        );
    }

    /**
     * Insert many entities
     *
     * @param {Array} entities
     * @returns {Promise<Array<Object>>} Return with the inserted documents in an Array.
     *
     * @memberof CouchDbNano
     */
    insertMany(entities) {
        return Promise.resolve(
            this.db.bulk({docs: entities})
                .then(result => result.map(el => el.id))
                .then(ids => this.findByIds(ids))
        );
    }

    /**
     * Update many entities by `selector` and `update`
     *
     * @param {Object} selector
     * @param {Object} update
     * @returns {Promise<Number>} Count of modified documents.
     *
     * @memberof CouchDbNano
     */
    updateMany(selector, update) {
        return this.find({selector})
            .then(docs => docs.map(doc => Object.assign({}, doc, update.$set ? update.$set : update)))
            .then(docs => this.db.bulk({docs}))
            .then(result => result.length);
    }

    /**
     * Remove entities which are matched by `selector`
     *
     * @param {Object} selector
     * @returns {Promise<Number>} Return with the count of deleted documents.
     *
     * @memberof CouchDbNano
     */
    removeMany(selector) {
        return this.find({selector})
            .then(docs => docs.map(doc => Object.assign({}, doc, {_deleted: true})))
            .then(docs => this.db.bulk({docs}))
            .then(result => result.length);
    }

    /**
     * Get count of filtered entites.
     *
     * Available filter props:
     * - selector (json) – JSON object describing criteria used to select documents. More information provided in the section on selector syntax. Required
     * - limit (number) – Maximum number of results returned. Default is 25. Optional
     * - skip (number) – Skip the first ‘n’ results, where ‘n’ is the value specified. Optional
     * - sort (json) – JSON array following sort syntax. Optional
     *
     * @param {Object} [filters={}]
     * @returns {Promise<Number>} Return with the count of documents.
     *
     * @memberof CouchDbNano
     */
    count(filters = {}) {
        return this.find(filters).then(docs => docs.length);
    }

    /**
     * Update an entity by ID and `update`
     *
     * @param {String} _id - ObjectID as hexadecimal string.
     * @param {Object} update
     * @returns {Promise<Object>} Return with the updated document.
     *
     * @memberof CouchDbNano
     */
    updateById(_id, update) {
        return this.findById(_id)
            .then(doc => Object.assign({}, doc, update.$set ? update.$set : update))
            .then(mergedDoc => this.db.insert(mergedDoc))
            .then(result => this.findById(result.id));
    }

    /**
     * Clear all entities from collection
     *
     * @returns {Promise}
     *
     * @memberof CouchDbNano
     */
    clear() {
        return this.removeMany({});
    }

    /**
     * Convert DB entity to JSON object. It converts the `_id` to hexadecimal `String`.
     *
     * @param {Object} entity
     * @returns {Object}
     * @memberof CouchDbNano
     */
    entityToObject(entity) {
        return _.cloneDeep(entity);
    }

    /**
     * Transforms 'idField' into CouchDB's '_id'
     * @param {Object} entity
     * @param {String} idField
     * @memberof CouchDbNano
     * @returns {Object} Modified entity
     */
    beforeSaveTransformID(entity, idField) {
        let newEntity = _.cloneDeep(entity);
        if (idField !== "_id" && newEntity[idField] !== undefined) {
            newEntity._id = newEntity[idField];
            delete newEntity[idField];
        }
        return newEntity;
    }

    /**
     * Transforms '_id' into user defined 'idField'
     * @param {Object} entity
     * @param {String} idField
     * @memberof CouchDbNano
     * @returns {Object} Modified entity
     */
    afterRetrieveTransformID(entity, idField) {
        if (idField !== "_id") {
            entity[idField] = entity["_id"];
            delete entity._id;
        }
        return entity;
    }
}

module.exports = CouchDbNanoAdapter;