WOrmMongodb.mjs

import events from 'events'
import mongodb from 'mongodb'
import stream from 'stream'
import cloneDeep from 'lodash-es/cloneDeep.js'
import get from 'lodash-es/get.js'
import map from 'lodash-es/map.js'
import omit from 'lodash-es/omit.js'
import size from 'lodash-es/size.js'
import genPm from 'wsemi/src/genPm.mjs'
import genID from 'wsemi/src/genID.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'


// //optMGConn
// let optMGConn = {
//     useNewUrlParser: true,
//     useUnifiedTopology: true
// }


/**
 * 操作資料庫(MongoDB)
 *
 * @class
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.url='mongodb://127.0.0.1:27017'] 輸入連接資料庫字串,預設'mongodb://127.0.0.1:27017'
 * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
 * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
 * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
 */
function WOrmMongodb(opt = {}) {


    //default
    if (!opt.url) {
        opt.url = 'mongodb://127.0.0.1:27017'
    }
    if (!opt.db) {
        opt.db = 'worm'
    }
    if (!opt.cl) {
        opt.cl = 'test'
    }


    //ee
    let ee = new events.EventEmitter()


    //MongoClient
    let MongoClient = mongodb.MongoClient


    /**
     * 查詢數據
     *
     * @memberOf WOrmMongodb
     * @param {Object} [find={}] 輸入查詢條件物件
     * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
     */
    async function select(find = {}) {
        let isErr = false

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)
            let collection = database.collection(opt.cl)

            //find
            let cursor = collection.find(find)
            //.sort({ $natural: -1 })
            //.limit(N)

            //toArray
            res = await cursor.toArray()

            //omit
            res = map(res, function(v) {
                v = omit(v, '_id')
                return v
            })

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
     *
     * @memberOf WOrmMongodb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
     */
    async function insert(data) {
        let isErr = false

        //check
        if (!iseobj(data) && !isearr(data)) {
            return {
                n: 0,
                nInserted: 0,
                ok: 1,
            }
        }

        //cloneDeep
        data = cloneDeep(data)

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)
            let collection = database.collection(opt.cl)

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //check id
            data = map(data, function(v) {
                if (!isestr(v.id)) {
                    v.id = genID()
                }
                return v
            })

            //insertMany
            res = await collection.insertMany(data)

            //check
            if (res.insertedCount > 0) {

                //res
                res = {
                    n: size(data),
                    nInserted: res.insertedCount,
                    ok: res.acknowledged ? 1 : 0,
                }

                //emit
                ee.emit('change', 'insert', data, res)

            }
            else {
                isErr = true

                //res
                res = `no insert data`

            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 儲存數據
     *
     * @memberOf WOrmMongodb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
     * @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
     */
    async function save(data, option = {}) {
        let isErr = false

        //check
        if (!iseobj(data) && !isearr(data)) {
            return []
        }

        //cloneDeep
        data = cloneDeep(data)

        //autoInsert
        let autoInsert = get(option, 'autoInsert', true)

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)
            let collection = database.collection(opt.cl)

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //check id
            data = map(data, function(v) {
                if (!isestr(v.id)) {
                    v.id = genID()
                }
                return v
            })

            //pmSeries
            res = await pmSeries(data, async(v) => {

                //rest
                let rest = null

                //oper
                rest = await collection.findOneAndUpdate({ id: v.id }, { $set: v })
                // console.log('rest', rest)

                if (iseobj(rest)) {
                    rest = {
                        n: 1,
                        nModified: 1,
                        ok: 1,
                    }
                }
                else {
                    rest = {
                        n: 0,
                        nModified: 0,
                        ok: 1,
                    }
                }

                //autoInsert
                if (autoInsert && rest.n === 0) {
                    rest = await insert(v)
                }

                return rest
            })

            //emit
            ee.emit('change', 'save', data, res)

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 刪除數據
     *
     * @memberOf WOrmMongodb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
     */
    async function del(data) {
        let isErr = false

        //check
        if (!iseobj(data) && !isearr(data)) {
            return []
        }

        //cloneDeep
        data = cloneDeep(data)

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)
            let collection = database.collection(opt.cl)

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //pmSeries
            res = await pmSeries(data, async(v) => {

                //rest
                let rest = null

                //deleteOne
                rest = await collection.deleteOne({ id: v.id })

                //rest
                rest = {
                    n: rest.deletedCount,
                    nDeleted: rest.deletedCount,
                    ok: rest.acknowledged ? 1 : 0,
                }

                return rest
            })

            //emit
            ee.emit('change', 'del', data, res)

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
     *
     * @memberOf WOrmMongodb
     * @param {Object} [find={}] 輸入刪除條件物件
     * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
     */
    async function delAll(find = {}) {
        let isErr = false

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)
            let collection = database.collection(opt.cl)

            //deleteMany
            res = await collection.deleteMany(find)

            //res
            res = {
                n: res.deletedCount,
                nDeleted: res.deletedCount,
                ok: res.acknowledged ? 1 : 0,
            }

            //emit
            ee.emit('change', 'delAll', null, res)

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }


        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 使用GridFS,插入數據,需為Uint8Array格式
     *
     * @memberOf WOrmMongodb
     * @param {Uint8Array} u8a
     * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
     */
    async function insertGfs(u8a) {
        let isErr = false

        //id
        let id = genID()

        //buf
        let buf = Buffer.from(u8a)

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //core
        let core = async (id, buf) => {

            //pm
            let pm = genPm()

            //database
            let database = client.db(opt.db)

            //bucket
            let bucket = new mongodb.GridFSBucket(database, {
                chunkSizeBytes: 10 * 1024 * 1024, //10mb
                bucketName: opt.cl
            })

            //stream
            let sm = new stream.Readable()
            sm._read = () => {}
            sm.push(buf)
            sm.push(null)
            sm.pipe(bucket.openUploadStream(id)) //pipe是接bucket的Writable, 所以會監聽finish
                .on('error', function(err) {

                    //reject
                    pm.reject(err)

                })
                .on('finish', function() {

                    //res
                    let res = { n: 1, ok: 1, id }

                    //resolve
                    pm.resolve(res)

                })

            return pm
        }

        //res
        let res = null
        try {

            //core
            res = await core(id, buf)

            //emit
            ee.emit('change', 'insertGfs', null, res)

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 使用GridFS,查詢數據
     *
     * @memberOf WOrmMongodb
     * @param {String} id 輸入查詢id字串
     * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
     */
    async function selectGfs(id) {
        let isErr = false

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //core
        let core = async (id) => {

            //pm
            let pm = genPm()

            //database
            let database = client.db(opt.db)

            //bucket
            let bucket = new mongodb.GridFSBucket(database, {
                chunkSizeBytes: 10 * 1024 * 1024, //10mb
                bucketName: opt.cl
            })

            //buf
            let buf = Buffer.from('')

            //stream
            let sm = bucket.openDownloadStreamByName(id)
            sm.on('data', function (chunk) {
                buf = Buffer.concat([buf, chunk])
            })
            sm.on('error', function (err) {

                //reject
                pm.reject(err)

            })
            sm.on('end', function () {

                //u8a
                let u8a = new Uint8Array(buf)

                //clean memory
                buf = null

                //resolve
                pm.resolve(u8a)

            })

            return pm
        }

        //res
        let res = null
        try {

            //core
            res = await core(id)

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    async function _findGfs(find = {}, bucket) {
        let isErr = false

        //res
        let res = null
        try {

            //find
            let cursor = bucket.find(find)

            //toArray
            res = await cursor.toArray()

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            // await client.close()
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 使用GridFS,刪除數據
     *
     * @memberOf WOrmMongodb
     * @param {String} id 輸入刪除id字串
     * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
     */
    async function delGfs(id) {
        let isErr = false

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)

            //bucket
            let bucket = new mongodb.GridFSBucket(database, {
                chunkSizeBytes: 10 * 1024 * 1024, //10mb
                bucketName: opt.cl
            })

            //_findGfs
            res = await _findGfs({ filename: id }, bucket)

            //check
            if (res.length === 0) {
                res = `can not find id[${id}]`
            }
            else if (res.length > 1) {
                res = `duplicate id[${id}]`
            }
            else {

                //bid, get _id from res[0]
                let bid = res[0]._id

                //delete
                res = await bucket.delete(bid)

                //res
                res = {
                    n: 1,
                    nDeleted: 1,
                    ok: 1,
                }

                //emit
                ee.emit('change', 'delGfs', null, res)

            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    async function _delGfs(bid, bucket) {
        let isErr = false

        //res
        let res = null
        try {

            //delete
            res = await bucket.delete(bid)

            //res
            res = {
                n: 1,
                nDeleted: 1,
                ok: 1,
            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            // await client.close()
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    /**
     * 使用GridFS,刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
     *
     * @memberOf WOrmMongodb
     * @param {Object} [find={}] 輸入刪除條件物件
     * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
     */
    async function delAllGfs(find = {}) {
        let isErr = false

        //client
        // let client = await MongoClient.connect(opt.url, optMGConn)
        // let client = new MongoClient(opt.url, optMGConn)
        let client = new MongoClient(opt.url)

        //res
        let res = null
        try {

            //database, collection
            let database = client.db(opt.db)

            //bucket
            let bucket = new mongodb.GridFSBucket(database, {
                chunkSizeBytes: 10 * 1024 * 1024, //10mb
                bucketName: opt.cl
            })

            //_findGfs
            res = await _findGfs(find, bucket)

            //n
            let n = size(res)

            //ps
            let ps = map(res, function(v) {
                let bid = v._id
                return _delGfs(bid, bucket)
            })

            //all
            res = await Promise.all(ps)

            //res
            res = {
                n,
                ok: 1
            }

            //emit
            ee.emit('change', 'delAllGfs', null, res)

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {
            await client.close()
            client = null
        }

        if (isErr) {
            return Promise.reject(res)
        }
        return res
    }


    //bind
    ee.select = select
    ee.insert = insert
    ee.save = save
    ee.del = del
    ee.delAll = delAll
    ee.selectGfs = selectGfs
    ee.insertGfs = insertGfs
    ee.delGfs = delGfs
    ee.delAllGfs = delAllGfs


    return ee
}


export default WOrmMongodb