WOrmReladb.mjs

import path from 'path'
import Sequelize from 'sequelize'
import cloneDeep from 'lodash-es/cloneDeep.js'
import get from 'lodash-es/get.js'
import map from 'lodash-es/map.js'
import each from 'lodash-es/each.js'
import size from 'lodash-es/size.js'
import split from 'lodash-es/split.js'
import keys from 'lodash-es/keys.js'
import pick from 'lodash-es/pick.js'
import omit from 'lodash-es/omit.js'
import isEqual from 'lodash-es/isEqual.js'
import evem from 'wsemi/src/evem.mjs'
import genPm from 'wsemi/src/genPm.mjs'
import genIDSeq from 'wsemi/src/genIDSeq.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isnum from 'wsemi/src/isnum.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import arrHas from 'wsemi/src/arrHas.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import pm2resolve from 'wsemi/src/pm2resolve.mjs'
import pmQueue from 'wsemi/src/pmQueue.mjs'
import WAutoSequelize from 'w-auto-sequelize/src/WAutoSequelize.mjs'
import importModels from './importModels.mjs'
import _genModelsByTabs from './genModelsByTabs.mjs'


//放置於全域宣告, 若使用useStable=true才可於當創建多實例時仍能全域控管
let pmq = pmQueue(1)


/**
 * 操作關聯式資料庫
 *
 * 注意: 各model內id欄位不是主鍵(primary key)時需要強制更改成為主鍵,否則sequelize無法匯入
 *
 * @class
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.url='mssql://username:password@localhost:1433'] 輸入連接資料庫字串,資料庫可選'mssql'、'sqlite'、'mysql'、'mariadb'、'postgres',預設'mssql://username:password@localhost:1433'
 * @param {String} [opt.storage='./worm.db'] 輸入為sqlite時,資料庫檔案所在位置字串,預設'./worm.db'
 * @param {Boolean} [opt.useEncryption=false] 輸入為sqlite時是否使用加密套件[@journeyapps/sqlcipher],型別為布林值,預設false,若是的話因依賴預設不安裝,得自行人工安裝
 * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
 * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
 * @param {String} [opt.fdModels='./models'] 輸入資料表設定檔所在資料夾字串,預設'./models'
 * @param {String} [opt.modelType='js'] 輸入資料表設定檔類型字串,可有'js'、'json',預設'js'
 * @param {Boolean} [opt.logging=false] 輸入是否輸出實際執行的sql指令布林值,預設false
 * @param {String} [opt.pk='id'] 輸入數據主鍵字串,預設'id'
 * @param {Boolean} [opt.autoGenPk=true] 輸入若數據pk(id)欄位沒給時則自動給予隨機uuid,型別為布林值,預設true
 * @param {Boolean} [opt.useStable=true] 輸入是否使用穩定模式,使用佇列管理同時只能進行一種操作故會犧牲效能,sqlite需開啟穩定模式才不會有非預期錯誤,型別為布林值,預設true
 * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
 */
function WOrmReladb(opt = {}) {
    let ss
    let u
    let sequelize = null


    //default
    if (!isestr(opt.url)) {
        opt.url = 'mssql://username:password@localhost:1433'
    }
    if (!isestr(opt.db)) {
        opt.db = 'worm'
    }
    if (!isestr(opt.cl)) {
        opt.cl = 'test'
    }
    if (!isestr(opt.fdModels)) {
        opt.fdModels = './models'
    }
    if (!isestr(opt.modelType)) {
        opt.modelType = 'js'
    }
    opt.logging = (opt.logging === true)
    if (!isestr(opt.pk)) {
        opt.pk = 'id'
    }
    if (!isbol(opt.autoGenPk)) {
        opt.autoGenPk = true
    }
    if (!isbol(opt.useStable)) {
        opt.useStable = true
    }
    if (!isestr(opt.storage)) {
        opt.storage = './worm.db' //storage, only use for sqlite
    }
    opt.storage = path.resolve(opt.storage)
    if (!isbol(opt.useEncryption)) {
        opt.useEncryption = false
    }


    //Op
    let Op = Sequelize.Op


    //ee, 採wsemi之evem(即eventemitter3), 其於'error'無監聽者時僅回傳false而不拋出,
    //故本套件之操作行為不因呼叫端有無註冊監聽而改變; Node內建之EventEmitter具該拋出語義, 不可用
    let ee = evem()


    //opt.url解析失敗一律拋出, 因其屬呼叫端未履行契約, 且解析失敗後各函數皆無從運作,
    //建構為同步而無從以Promise.reject回報, 拋出即為reject之同步對應;
    //若僅console.log而回傳未綁定各函數之ee, 錯誤不經任何管道抵達呼叫端, 呼叫端僅得到
    //[w.select is not a function]之無關訊息, 與本規格[錯誤不得靜默]之通則相違
    let throwUrlErr = (msg) => {
        throw new Error(`${msg}, opt.url[${opt.url}]`)
    }


    //dialect
    let dialect
    ss = split(opt.url, '://')
    dialect = get(ss, 0, null)
    if (!dialect) {
        throwUrlErr('no dialect in opt.url')
    }
    if (!arrHas(dialect, ['mssql', 'sqlite', 'mysql', 'mariadb', 'postgres'])) {
        throwUrlErr('invalid dialect in opt.url')
    }
    u = get(ss, 1, '') //另存給後面使用


    //username, password
    let username
    let password
    ss = split(u, '@')
    u = get(ss, 1, '') //另存給後面使用
    ss = get(ss, 0, '')
    ss = split(ss, ':')
    if (size(ss) !== 2) {
        throwUrlErr('invalid username or password in opt.url')
    }
    username = get(ss, 0, '')
    password = get(ss, 1, '')
    if (dialect !== 'sqlite' || (dialect === 'sqlite' && opt.useEncryption)) {
        if (username === '') {
            throwUrlErr('invalid username in opt.url')
        }
        if (password === '') {
            throwUrlErr('invalid password in opt.url')
        }
    }


    //host, port
    let host
    let port
    ss = split(u, ':')
    host = get(ss, 0, null)
    port = get(ss, 1, null)
    if (dialect !== 'sqlite') {
        if (!host) {
            throwUrlErr('invalid host in opt.url')
        }
        if (!port) {
            throwUrlErr('invalid port in opt.url')
        }
    }


    /**
     * 取錯誤訊息字串,供逐筆結果之err欄位與error事件使用
     *
     * @ignore
     * @param {Error|String} err 輸入錯誤物件或字串
     * @returns {String} 回傳錯誤訊息字串
     */
    function getErrMsg(err) {

        //message
        let message = get(err, 'message')
        if (isestr(message)) {
            return message
        }

        return String(err)
    }


    /**
     * 發出change事件,訂閱函數拋錯不得影響本次操作之結果,故另包try並自行吞掉
     *
     * @ignore
     * @param {String} mode 輸入操作別字串
     * @param {Object|Array|null} data 輸入本次操作之數據
     * @param {Object|Array} res 輸入本次操作之結果
     * @returns {undefined} 無回傳值
     */
    function emitChange(mode, data, res) {
        try {
            ee.emit('change', mode, data, res)
        }
        catch (err) {
            console.log(err)
        }
    }


    /**
     * 發出error事件,操作發生錯誤時發出,錯誤訊息一律轉為字串
     * 註: 事件僅為附加通知,所送出之資訊必另有正規管道——整批性錯誤經Promise.reject,逐筆失敗經該筆之err欄位
     * 註: 訂閱函數拋錯不得影響本次操作之結果,故另包try並自行吞掉
     * 註: 正常結果不得發出本事件,如insert全數已存在、save合併後內容相同、del主鍵未命中、selectByPk查無數據
     *
     * @ignore
     * @param {String} mode 輸入操作別字串
     * @param {Object|Array|null} data 輸入本次操作之數據
     * @param {Error|String} err 輸入錯誤物件或字串
     * @returns {undefined} 無回傳值
     */
    function emitError(mode, data, err) {
        try {
            ee.emit('error', mode, data, getErrMsg(err))
        }
        catch (errEmit) {
            console.log(errEmit)
        }
    }


    /**
     * 判定主鍵值是否有效
     * 註: select以外之五函數對主鍵之認定一律以本函數為準,令selectByPk之[命中]與insert、save、del內對既有數據之認定一致
     *
     * @ignore
     * @param {*} v 輸入主鍵值
     * @returns {Boolean} 回傳主鍵值是否有效布林值
     */
    function isEffPk(v) {

        //非空字串
        if (isestr(v)) {
            return true
        }

        //數值, 含0, 供整數型主鍵使用
        if (isnum(v)) {
            return true
        }

        return false
    }


    /**
     * 判定是否為主鍵重複之錯誤
     * 註: sequelize於mssql、sqlite、mysql、mariadb、postgres皆將唯一鍵衝突轉為UniqueConstraintError,故此判定與dialect無關
     *
     * @ignore
     * @param {Error} err 輸入錯誤物件
     * @returns {Boolean} 回傳是否為主鍵重複錯誤布林值
     */
    function isDupPkError(err) {
        return err instanceof Sequelize.UniqueConstraintError
    }


    /**
     * 檢查呼叫端所給之實例是否已關閉,已關閉者拋出令各函數於入口快速失敗
     * 註: 實例已關閉為終態,後續操作必然失敗,屬影響全部數據之整批性錯誤,
     * 故不得降為逐筆ok為0而整批resolve,亦不得以正常空結果(null、未命中、空陣列)回應——
     * 後者使[已關閉]與[查無資料]同形,去重類呼叫端將把每筆判為未存在而靜默fail-open
     * 註: closeSequelize於關閉後將sequelize設為null,故據此判定即為精確訊號,
     * 不須倚賴驅動層之錯誤訊息比對(連線管理器關閉後所拋者為原生Error而非sequelize之錯誤類別)
     * 註: 內部自行初始化者(instance為null)每次呼叫各自開關連線,不受本檢查影響
     *
     * @ignore
     * @param {Object|null} instance 輸入呼叫端所給之實例物件
     * @returns {undefined} 無回傳值
     */
    function procClosed(instance) {

        //check
        if (instance === null) {
            return
        }

        //check
        if (sequelize === null) {
            throw new Error(`instance is closed, need to use init() to get a new instance`)
        }

    }


    /**
     * 判定錯誤是否影響後續所有筆數而應視為整批性錯誤
     * 註: 依T4之判別原則,錯誤只影響該筆資料者為逐筆失敗,影響後續所有筆數者為整批性錯誤;
     * 連線層錯誤屬後者,故於逐筆函數內攔得時須往外拋而不得降為該筆ok為0
     * 註: 連線管理器關閉後所拋者為原生Error而非sequelize之ConnectionError,故另以訊息辨識
     *
     * @ignore
     * @param {Error} err 輸入錯誤物件
     * @returns {Boolean} 回傳是否為整批性錯誤布林值
     */
    function isBatchLevelError(err) {

        //check, sequelize之連線層錯誤家族
        if (err instanceof Sequelize.ConnectionError) {
            return true
        }

        //msg
        let msg = getErrMsg(err)

        //check
        if (msg.indexOf('connection manager was closed') >= 0) {
            return true
        }
        if (msg.indexOf('SQLITE_MISUSE') >= 0) {
            return true
        }

        return false
    }


    /**
     * 檢查並補齊單筆數據之主鍵
     * autoGenPk為true時未帶有效主鍵者自動產生,為false時往外拋
     * 註: 未帶有效主鍵屬呼叫端未履行契約而非某一筆資料本身之問題,故為整批性錯誤而不降級為該筆ok為0,
     * 若降級為逐筆結果,呼叫端易於整批resolve之下漏看,使[忘了給主鍵]靜默變成[少寫了幾筆]
     * 註: 本函數須於任何寫入之前一次對全部數據完成,令拋錯時同批之有效筆數亦不會被寫入
     * 註: 自動產生之主鍵值為字串,故補值前檢查主鍵欄位確為字串類型,不符者以明確訊息拋出,
     * 而非任由資料庫回報型別錯誤; 本套件之model定義於手上得知主鍵型別,故不適用T6之[套件無從得知主鍵型別]例外
     *
     * @ignore
     * @param {Object} md 輸入資料表model物件
     * @param {Object} v 輸入數據物件
     * @param {Number} k 輸入數據於陣列內之索引
     * @returns {Object} 回傳補齊主鍵之數據物件
     */
    function procPk(md, v, k) {

        //check
        if (isEffPk(get(v, opt.pk))) {
            return v
        }

        //check, autoGenPk為false時主鍵須由呼叫端自備
        if (!opt.autoGenPk) {
            throw new Error(`invalid data[${k}].${opt.pk}, autoGenPk is false`)
        }

        //at
        let at = get(md, `rawAttributes.${opt.pk}`)
        if (!iseobj(at)) {
            throw new Error(`can not find pk column [${opt.pk}] in model [${opt.cl}]`)
        }

        //check, 自動產生之主鍵值為字串, 主鍵欄位須為字串類型方能容納
        let tp = get(at, 'type.key', '')
        if (!arrHas(tp, ['STRING', 'TEXT', 'CHAR', 'UUID', 'CITEXT'])) {
            throw new Error(`autoGenPk is true but pk column [${opt.pk}] in model [${opt.cl}] is [${tp}], auto generated pk is a string, need to set autoGenPk to false and supply pk by caller`)
        }

        //genIDSeq, 為UUIDv7格式而具單調遞增性, 主鍵接近順序遞增可減少索引頁面分裂
        v[opt.pk] = genIDSeq()

        return v
    }


    /**
     * 初始化sequelize
     *
     * @memberOf WOrmReladb
     * @param {Boolean} [sync=false] 輸入當importModels匯入models時是否使用同步方式,將models資料變更至資料庫當中,此功能提供給createStorage之用
     * @returns {Promise} 回傳Promise,resolve代表關閉成功,reject回傳錯誤訊息
     */
    async function initSequelize(sync = false) {
        let err = null

        //optSeq
        let optSeq = {
            dialect,
            host,
            port,
            storage: opt.storage,
            logging: opt.logging,
            define: {
                //underscored: false,
                //freezeTableName: false,
                //syncOnAssociation: true,
                //charset: 'utf8',
                //dialectOptions: {
                //  collate: 'utf8_general_ci'
                //},
                timestamps: false
            },
            // pool: {
            //     max: 20,
            //     min: 0,
            //     acquire: 30000, //The maximum time, in milliseconds, that pool will try to get connection before throwing error
            //     idle: 10000 //The maximum time, in milliseconds, that a connection can be idle before being released.
            // },
            //sync: { force: true }, //強制同步
        }
        if (opt.useEncryption && dialect === 'sqlite') {
            optSeq.dialectModulePath = '@journeyapps/sqlcipher' //sqlite加密版
        }

        //sequelize
        sequelize = new Sequelize(opt.db, username, password, optSeq)
        if (opt.useEncryption && dialect === 'sqlite') {
            await sequelize.query('PRAGMA cipher_compatibility = 4') //設置sqlite密鑰相容性
            await sequelize.query(`PRAGMA key = '${username}:${password}'`) //設置slqite密碼為${username}:${password}, 為使用者名稱與密碼用冒號分隔
        }

        //importModels, 若model內id不是pk則需要強制更改成為pk, 否則sequelize無法匯入
        let r = await pm2resolve(importModels)(opt.fdModels, sequelize, opt.cl, { sync, type: opt.modelType })

        //check
        if (r.state === 'error') {
            console.log(r)
            err = `can not import model: ${opt.cl}, need to use genModelsByDB, genModelsByTabs or create ${opt.cl}.${opt.modelType}`
        }

        //mds
        let mds = r.msg

        return {
            mds,
            err,
            close: () => {
                return closeSequelize('external')
            },
        }
    }


    /**
     * 關閉sequelize
     *
     * @memberOf WOrmReladb
     * @returns {Promise} 回傳Promise,resolve代表關閉成功,reject回傳錯誤訊息
     */
    async function closeSequelize(from) {
        if (sequelize !== null) {
            await sequelize.close()
            //console.log(from, 'sequelize.close()')
        }
        sequelize = null
        //console.log(from, 'sequelize = null')
    }


    /**
     * 產生交易transaction狀態物件,當使用transaction時資料庫會上鎖,只能供調用的連線操作處理
     *
     * @memberOf WOrmReladb
     * @returns {Promise} 回傳Promise,resolve回傳交易transaction物件,reject回傳錯誤訊息
     */
    async function genTransaction() {
        let t
        if (sequelize !== null) {
            t = await sequelize.transaction() //使用Unmanaged transaction (then-callback)
            // t.afterCommit(() => {
            //     console.log('afterCommit')
            // })
        }
        else {
            return Promise.reject('invalid sequelize')
        }
        return t
    }


    /**
     * 查詢數據
     *
     * @memberOf WOrmReladb
     * @param {Object} [find={}] 輸入查詢條件物件
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
     */
    async function select(find = {}, option = {}) {

        function cvObj(o) {
            let oNew = {}
            each(o, (v, k) => {
                let kNew = k
                if (k.indexOf('$') >= 0) {
                    k = k.replace('$', '')
                    if (k === 'regex') {
                        kNew = Op.substring
                    }
                    else if (k === 'options') {
                        kNew = null
                    }
                    else if (k === 'nin') {
                        kNew = Op.notIn
                    }
                    else {
                        kNew = Op[k]
                    }
                }
                let vNew = v
                if (isarr(v)) {
                    vNew = cvArray(v)
                }
                else if (isobj(v)) {
                    vNew = cvObj(v)
                }
                if (kNew !== null) {
                    oNew[kNew] = vNew
                }
            })
            return oNew
        }

        function cvArray(o) {
            let oNew = []
            each(o, (v) => {
                let vNew = v
                if (isarr(v)) {
                    vNew = cvArray(v)
                }
                else if (isobj(v)) {
                    vNew = cvObj(v)
                }
                oNew.push(vNew)
            })
            return oNew
        }

        function cvFind(o) {
            let oNew = cvObj(o)
            return oNew
        }

        //find
        if (!isobj(find)) {
            find = {}
        }
        find = cloneDeep(find)

        //useFind
        let useFind = cvFind(find)

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 連線或匯入models失敗影響本次操作之全部數據, 屬整批性錯誤
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

            //setting, raw令回傳純數據物件而不含資料庫內部欄位
            let setting = {
                where: useFind,
                raw: true,
            }
            if (transaction !== null) {
                setting.transaction = transaction
            }

            //findAll, 恆回傳陣列, 無符合數據為空陣列
            res = await md.findAll(setting)
            if (!isarr(res)) {
                res = []
            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('select')
            }

        }

        //check
        if (isErr) {
            emitError('select', null, res)
            return Promise.reject(res)
        }

        return res
    }


    /**
     * 由主鍵查詢單筆數據,因由資料庫依主鍵索引取值且僅回傳單筆,不需如select提取全部符合數據再處理,故數據量大時效能較佳
     *
     * 註: 主鍵欄位取自opt.pk,預設為id,支援由呼叫端指定
     * 註: 主鍵未命中或主鍵值無效皆回傳null而不reject,[命中]之判定基準與insert、save、del內對既有數據之認定一致
     * 註: 本函數不得有副作用,故不創建資料表
     *
     * @memberOf WOrmReladb
     * @param {String|Number} pk 輸入主鍵值,即數據內opt.pk欄位之值
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵或主鍵值無效則回傳null,reject回傳錯誤訊息
     */
    async function selectByPk(pk, option = {}) {

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check, 實例已關閉之判定須先於[主鍵值無效回null], 否則兩者同形而使[已關閉]被誤讀為[查無資料]
        try {
            procClosed(instance)
        }
        catch (err) {
            emitError('selectByPk', null, err)
            return Promise.reject(err)
        }

        //check, 未給有效主鍵值視為查無數據, 不送查詢以免無效值被轉為null而誤中其他數據
        if (!isEffPk(pk)) {
            return null
        }

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
            procClosed(instance)

            //check
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

            //setting
            let setting = {
                where: { [opt.pk]: pk },
                raw: true,
            }
            if (transaction !== null) {
                setting.transaction = transaction
            }

            //findOne
            let v = await md.findOne(setting)

            //check, 判定基準與insert、save、del內對既有數據之認定一致
            if (iseobj(v)) {
                res = v
            }
            else {
                //不存在主鍵, 回傳null
                res = null
            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('selectByPk')
            }

        }

        //check
        if (isErr) {
            emitError('selectByPk', null, res)
            return Promise.reject(res)
        }

        return res
    }


    /**
     * 插入數據,僅於主鍵不存在時寫入,已存在者跳過且不覆寫
     *
     * 由資料庫於主鍵之唯一約束上原子完成[檢查主鍵不存在]與[寫入],併發時同一主鍵僅有一次成功
     *
     * 註: n為輸入筆數即本次嘗試插入之基準,nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果
     * 註: 同批含重複主鍵時僅首筆計入nInserted,其餘視為已存在
     * 註: opt.autoGenPk為true(預設)時未帶有效主鍵者自動產生,為false時未帶有效主鍵即reject且同批皆不寫入
     * 註: option.returnList開啟時改回與輸入等長且保序之逐筆結果陣列[{n,nInserted,ok}],
     * 聚合計數只答得出[有幾筆是新的]而答不出[是哪幾筆],而後者正是去重之產出物;
     * 逐筆元素之n與ok恆為1,資訊由nInserted承載,不變式為
     * 陣列長度等於輸入筆數且filter(v => v.nInserted === 1).length等於聚合模式之nInserted
     *
     * @memberOf WOrmReladb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @param {Boolean} [option.returnList=false] 輸入是否改回逐筆結果陣列布林值,預設false
     * @returns {Promise} 回傳Promise,resolve回傳插入結果{n,nInserted,ok},或於returnList開啟時回傳[{n,nInserted,ok}],reject回傳錯誤訊息
     */
    async function insert(data, option = {}) {

        //returnList, 回傳形式之切換為靜態, 僅由本選項之取值決定, 不因數據內容或執行結果而變
        let returnList = get(option, 'returnList', false) === true

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

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

        //cloneDeep
        data = cloneDeep(data)

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
            procClosed(instance)

            //check
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

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

            //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批之有效筆數亦不會被寫入
            data = map(data, (v, k) => {
                return procPk(md, v, k)
            })

            //nAll, n之基準為輸入筆數
            let nAll = size(data)

            //setting
            let setting = { }
            if (transaction !== null) {
                setting.transaction = transaction
            }

            //create, 逐筆插入以取得精確之nInserted
            //註: 不採bulkCreate配合ignoreDuplicates, 因mssql未宣告該能力而選項會被靜默忽略仍撞唯一約束,
            //且bulkCreate無從回報實際插入筆數; 單筆INSERT本即由主鍵之唯一約束原子完成檢查與寫入
            //註: bIns即逐筆判定, 與輸入等長且保序, 供returnList直接包裝而不須另行推導
            let bInss = await pmSeries(data, async(v) => {

                //bIns, 撞既有主鍵者視為已存在而跳過, 其餘錯誤影響全部數據須往外拋
                let bIns = await md.create(v, setting)
                    .then(() => {
                        return true
                    })
                    .catch((err) => {
                        if (isDupPkError(err)) {
                            return false
                        }
                        throw err
                    })

                return bIns
            })

            if (returnList) {

                //res, 與輸入等長且保序之逐筆結果; 各筆之n恆為1(該筆主鍵或為命中既有或經插入而產生),
                //ok恆為1(insert之任何錯誤皆屬整批性錯誤而reject), 資訊由nInserted承載
                res = map(bInss, (bIns) => {
                    return {
                        n: 1,
                        nInserted: bIns ? 1 : 0,
                        ok: 1,
                    }
                })

            }
            else {

                //res, 全數已存在而nInserted為0屬正常結果, 不視為錯誤
                res = {
                    n: nAll,
                    nInserted: size(bInss.filter((v) => v === true)),
                    ok: 1,
                }

            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('insert')
            }

        }

        //check
        if (isErr) {
            emitError('insert', data, res)
            return Promise.reject(res)
        }

        //emit, 須於結果定案後發出
        emitChange('insert', data, res)

        return res
    }


    /**
     * 批次插入數據,全批視為一個單位:全部插入成功,或一筆都不寫入
     *
     * 本函數非insert之加速版,兩者衝突政策不同:insert於主鍵已存在時跳過該筆且整批ok為1,
     * 本函數則整批reject且不寫入任何一筆,同批含重複主鍵者亦視為衝突。
     * 確無衝突時兩者之可觀察結果完全相同,差異僅於有衝突時顯現。
     *
     * 註: n為輸入筆數即本次嘗試插入之基準;nInserted於成功時恆等於n,雖無額外資訊仍保留,
     * 令呼叫端得與insert共用同一段結果處理程式碼
     * 註: 不提供逐筆結果,故不出現ok為0與err;需要逐筆處置者改用insert
     * 註: opt.autoGenPk為true(預設)時未帶有效主鍵者自動產生,為false時未帶有效主鍵即reject且同批皆不寫入
     * 註: [全有全無]為獨立於T7之額外要求——T7僅保證每筆之[檢查主鍵不存在]與[寫入]為原子,不保證整批。
     * mssql因綁定參數數量上限會由驅動層自動拆為多語句送出,實測1000筆之批次於末筆撞主鍵時已有946筆落盤,
     * 故一律以交易包覆並於失敗時回滾;呼叫端已給transaction時改以巢狀交易(SAVEPOINT)包覆,
     * 令回滾範圍限於本次呼叫而不影響呼叫端交易內先前之寫入
     *
     * @memberOf WOrmReladb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @returns {Promise} 回傳Promise,resolve回傳插入結果{n,nInserted,ok},reject回傳錯誤訊息
     */
    async function insertBulk(data, option = {}) {

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

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

        //cloneDeep
        data = cloneDeep(data)

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
            procClosed(instance)

            //check
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

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

            //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批皆不會被寫入
            data = map(data, (v, k) => {
                return procPk(md, v, k)
            })

            //nAll, n之基準為輸入筆數
            let nAll = size(data)

            //useTx, sqlcipher不支援transaction, 該情形下改倚賴sqlite單一語句之原子性
            let useTx = !(opt.useEncryption && dialect === 'sqlite')

            //t, 呼叫端未給交易時自開; 已給時以巢狀交易(SAVEPOINT)包覆令回滾範圍限於本次呼叫
            let t = null
            if (useTx) {
                t = (transaction !== null)
                    ? await md.sequelize.transaction({ transaction })
                    : await md.sequelize.transaction()
            }

            //setting
            let setting = {}
            if (t !== null) {
                setting.transaction = t
            }
            else if (transaction !== null) {
                setting.transaction = transaction
            }

            try {

                //bulkCreate, 未加任何跳過選項, 故任一筆撞主鍵即整句失敗, 同批重複主鍵亦於此被偵測為衝突
                await md.bulkCreate(data, setting)

                if (t !== null) {
                    await t.commit()
                }

            }
            catch (err) {
                if (t !== null) {
                    await t.rollback().catch(() => {})
                }
                throw err
            }

            //res, 全有全無故成功時nInserted恆等於n
            res = {
                n: nAll,
                nInserted: nAll,
                ok: 1,
            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('insertBulk')
            }

        }

        //check
        if (isErr) {
            emitError('insertBulk', data, res)
            return Promise.reject(res)
        }

        //emit, 須於結果定案後發出
        emitChange('insertBulk', data, res)

        return res
    }


    /**
     * 儲存單筆數據
     *
     * 採條件寫入配合衝突偵測與重試: 每次寫入本身為單一條件式原子語句——插入由主鍵之唯一約束判定、
     * 更新由WHERE主鍵之比對判定,皆非由預讀值決定其成敗;預讀僅用於選擇發出哪一條語句,
     * 選錯不會產生錯誤結果,只會被偵測並重試(插入撞既有主鍵、更新未命中任何列),故縱使預讀值已過期,
     * 最終結果仍與該次操作單獨執行時相同。
     * 註: 不採sequelize之upsert,因sqlite無從於單一語句內回報本次係插入或更新(其created恆為null),
     * 而insert與save之規格要求nInserted精確,兩者於sqlite互斥,故改採本形式
     * 註: [內容相同]之判定基準為將待寫入物件合併進現值後與現值相同,相同則不寫入而nModified為0,
     * 合併取淺層以與後端整欄取代之寫入行為一致,令nModified忠實反映資料庫端是否真的寫入
     *
     * @ignore
     * @param {Object} md 輸入資料表model物件
     * @param {Object} v 輸入數據物件
     * @param {Boolean} autoInsert 輸入是否於查無數據時自動改以插入處理布林值
     * @param {Object|null} transaction 輸入交易(transaction)物件
     * @param {Array} ks 輸入model之欄位名陣列
     * @returns {Promise} 回傳Promise,resolve回傳本筆儲存結果,本筆失敗時亦resolve並以ok為0回報
     */
    async function saveOne(md, v, autoInsert, transaction, ks) {

        //nTry, 預測錯誤時重試, 給予有限次數令其收斂
        let nTry = 3

        //rest
        let rest = null

        //inserted, 供結果定案後發出insert事件用
        let inserted = false

        //pkv
        let pkv = get(v, opt.pk)

        //setting
        let settingRead = {
            where: { [opt.pk]: pkv },
            raw: true,
        }
        let settingUpd = {
            where: { [opt.pk]: pkv },
        }
        let settingIns = {}
        if (transaction !== null) {
            settingRead.transaction = transaction
            settingUpd.transaction = transaction
            settingIns.transaction = transaction
        }

        for (let i = 0; i < nTry; i++) {

            try {

                //預讀, 供判斷內容是否相同之快速路徑, 其結果不用於決定寫入內容
                let _v = await md.findOne(settingRead)

                if (iseobj(_v)) {
                    //主鍵存在

                    //_vt, vt, 以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對
                    let _vt = omit(_v, opt.pk)
                    let vt = {
                        ..._vt,
                        ...pick(omit(v, opt.pk), ks),
                    }

                    //check, 內容相同者不寫入
                    if (isEqual(vt, _vt)) {
                        rest = {
                            n: 1,
                            nInserted: 0,
                            nModified: 0,
                            ok: 1,
                        }
                        break
                    }

                    //update, 單一UPDATE語句由WHERE主鍵原子完成查找與更新
                    let rr = await md.update(v, settingUpd)
                    let nAff = get(rr, 0, 0)

                    if (nAff > 0) {
                        rest = {
                            n: 1,
                            nInserted: 0,
                            nModified: 1,
                            ok: 1,
                        }
                        break
                    }

                    //該列已被他方刪除, 重試改走插入
                    continue
                }

                //主鍵不存在且未開啟autoInsert則不寫入以免無中生有
                if (!autoInsert) {
                    rest = {
                        n: 0,
                        nInserted: 0,
                        nModified: 0,
                        ok: 1,
                    }
                    break
                }

                //create, 單一INSERT語句由主鍵之唯一約束原子完成檢查與寫入
                let bIns = await md.create(v, settingIns)
                    .then(() => {
                        return true
                    })
                    .catch((err) => {
                        if (isDupPkError(err)) {
                            return false
                        }
                        throw err
                    })

                if (bIns) {
                    rest = {
                        n: 1,
                        nInserted: 1,
                        nModified: 0,
                        ok: 1,
                    }
                    inserted = true
                    break
                }

                //他方已插入同一主鍵, 重試改走更新
                continue
            }
            catch (err) {

                //check, 連線層錯誤影響後續所有筆數, 依T4屬整批性錯誤, 往外拋而不降為本筆ok為0
                if (isBatchLevelError(err)) {
                    throw err
                }

                //其餘錯誤視為本筆失敗, 不中斷整批, 由呼叫端以ok與err判讀
                rest = {
                    n: 1,
                    nInserted: 0,
                    nModified: 0,
                    ok: 0,
                    err: getErrMsg(err),
                }
                break
            }

        }

        //check, 重試耗盡仍未定案
        if (rest === null) {
            rest = {
                n: 1,
                nInserted: 0,
                nModified: 0,
                ok: 0,
                err: `can not save data by ${opt.pk}[${pkv}] in ${nTry} tries`,
            }
        }

        //emit, 須於本筆結果定案後發出, 避免訂閱函數拋錯影響本筆結果
        if (inserted) {
            emitChange('insert', [v], rest)
        }
        if (rest.ok === 0) {
            emitError('save', [v], rest.err)
        }

        return rest
    }


    /**
     * 儲存數據,以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且option.autoInsert為true(預設)時改為插入
     *
     * 註: 回傳陣列恆與輸入等長,輸入單一物件亦回傳長度1之陣列
     * 註: n為主鍵命中筆數,值為0或1,命中(不論內容有無變更)或經插入而產生皆為1;nInserted與nModified恆同時出現
     * 註: [內容相同]之判定基準為將待寫入物件合併進現值後與現值相同,非待寫入物件與現值全等,
     * 故只給部份欄位且該些欄位值皆與現值相同時,合併結果等於現值,nModified為0
     * 註: 本筆失敗不中斷整批,該筆以ok為0並附err回報
     * 註: opt.autoGenPk為true(預設)時未帶有效主鍵者自動產生,為false時未帶有效主鍵即reject且同批皆不寫入
     *
     * @memberOf WOrmReladb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
     * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之儲存結果陣列[{n,nInserted,nModified,ok}],reject回傳錯誤訊息
     */
    async function save(data, option = {}) {

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

        //cloneDeep
        data = cloneDeep(data)

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

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

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
            procClosed(instance)

            //check
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

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

            //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批之有效筆數亦不會被寫入
            data = map(data, (v, k) => {
                return procPk(md, v, k)
            })

            //ks, model之欄位名, 供合併後比對時濾除非欄位之鍵
            let ks = keys(get(md, 'rawAttributes', {}))

            //pmSeries
            res = await pmSeries(data, async(v) => {
                return saveOne(md, v, autoInsert, transaction, ks)
            })

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('save')
            }

        }

        //check
        if (isErr) {
            emitError('save', data, res)
            return Promise.reject(res)
        }

        //emit, 須於結果定案後發出
        emitChange('save', data, res)

        return res
    }


    /**
     * 刪除數據
     *
     * 註: 回傳陣列恆與輸入等長,各筆之n與nDeleted皆為主鍵命中筆數,值為0或1,未命中為0且屬正常結果
     * 註: 判斷某筆是否真的被刪除一律以nDeleted為準
     * 註: 未帶有效主鍵者為該筆之輸入問題,回ok為0與err且不送查詢條件(以免無效值被轉為null而誤中其他數據),
     * 不中斷其餘筆數,以此與[主鍵未命中]之ok為1區辨
     * 註: 本函數不受opt.autoGenPk影響,於任一設定下皆不補值
     *
     * @memberOf WOrmReladb
     * @param {Object|Array} data 輸入數據物件或陣列,會查找各數據的opt.pk值,有存在者就刪除
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列[{n,nDeleted,ok}],reject回傳錯誤訊息
     */
    async function del(data, option = {}) {

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

        //cloneDeep
        data = cloneDeep(data)

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
            procClosed(instance)

            //check
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

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

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

                //rest
                let rest = null

                //pkv
                let pkv = get(v, opt.pk)

                //check, 未帶有效主鍵者不補值亦不送查詢, 直接視為該筆無法處理
                if (!isEffPk(pkv)) {

                    //rest
                    rest = {
                        n: 0,
                        nDeleted: 0,
                        ok: 0,
                        err: `invalid ${opt.pk}[${pkv}]`,
                    }

                    //emit
                    emitError('del', [v], rest.err)

                    return rest
                }

                //setting
                let setting = {
                    where: { [opt.pk]: pkv },
                }
                if (transaction !== null) {
                    setting.transaction = transaction //destroy的說明寫不需transaction但實際需要 (2020/10/07)
                }

                //destroy, 單一DELETE語句即可取得命中筆數, 不另行findOne以免多一次往返且與刪除間存在競態
                await md.destroy(setting)
                    .then((r) => {

                        //nDel, n與nDeleted皆為主鍵命中筆數, 未命中為0且屬正常結果
                        let nDel = isnum(r) ? r : 0

                        //rest
                        rest = {
                            n: nDel,
                            nDeleted: nDel,
                            ok: 1,
                        }

                    })
                    .catch((err) => {

                        //check, 連線層錯誤影響後續所有筆數, 依T4屬整批性錯誤, 往外拋而不降為本筆ok為0
                        if (isBatchLevelError(err)) {
                            throw err
                        }

                        //rest, 本筆失敗不中斷整批
                        rest = {
                            n: 1,
                            nDeleted: 0,
                            ok: 0,
                            err: getErrMsg(err),
                        }

                        //emit
                        emitError('del', [v], rest.err)

                    })

                return rest
            })

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('del')
            }

        }

        //check
        if (isErr) {
            emitError('del', data, res)
            return Promise.reject(res)
        }

        //emit, 須於結果定案後發出
        emitChange('del', data, res)

        return res
    }


    /**
     * 依條件刪除多筆數據,需與del分開,避免未傳數據導致直接刪除全表
     *
     * 註: n與nDeleted皆為實際刪除筆數,兩者恆相等,n不得取全表筆數
     * 註: find未給或為空物件時刪除全部數據;條件無命中時回{n:0,nDeleted:0,ok:1},不視為錯誤
     *
     * @memberOf WOrmReladb
     * @param {Object} [find={}] 輸入刪除條件物件,不給予find則代表刪除全部數據
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
     * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
     * @returns {Promise} 回傳Promise,resolve回傳刪除結果{n,nDeleted,ok},reject回傳錯誤訊息
     */
    async function delAll(find = {}, option = {}) {

        //find
        if (!isobj(find)) {
            find = {}
        }
        find = cloneDeep(find)

        //instance
        let instance = get(option, 'instance', null)

        //transaction
        let transaction = get(option, 'transaction', null)

        //check
        if (opt.useEncryption && dialect === 'sqlite') {
            if (transaction !== null) {
                console.log('@journeyapps/sqlcipher can not support transaction.')
            }
        }

        //isErr, res
        let isErr = false
        let res = null

        //si
        let si = instance
        if (instance === null) {
            si = await initSequelize()
        }

        try {

            //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
            procClosed(instance)

            //check
            if (si.err) {
                throw si.err
            }

            //md
            let md = si.mds[opt.cl]

            //setting
            let setting = {
                where: find,
            }
            if (transaction !== null) {
                setting.transaction = transaction //destroy的說明寫不需transaction但實際需要 (2020/10/07)
            }

            //destroy, n與nDeleted皆為實際刪除筆數, 條件無命中時為0且屬正常結果
            let r = await md.destroy(setting)
            let nDel = isnum(r) ? r : 0

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

        }
        catch (err) {
            isErr = true
            res = err
        }
        finally {

            //closeSequelize
            if (instance === null) { //內部自動初始化得close
                await closeSequelize('delAll')
            }

        }

        //check
        if (isErr) {
            emitError('delAll', null, res)
            return Promise.reject(res)
        }

        //emit, 須於結果定案後發出
        emitChange('delAll', null, res)

        return res
    }


    /**
     * 創建sqlite資料庫檔案
     *
     * @memberOf WOrmReladb
     * @returns {Promise} 回傳Promise,resolve回傳創建結果,reject回傳錯誤訊息
     */
    async function createStorage() {

        //pm
        let pm = genPm()

        //initSequelize
        let si = await initSequelize(true)

        //closeSequelize
        await closeSequelize('createStorage')

        //check
        if (si.err) {
            pm.reject(si.err)
        }
        else {
            pm.resolve('created')
        }

        return pm
    }


    /**
     * 由指定資料庫生成各資料表的models資料
     *
     * 目前僅能產生js檔格式,且mssql欄位為nvarchar(MAX)無法自動轉成TEXT格式
     *
     * include from: [w-auto-sequelize](https://github.com/yuda-lyu/w-auto-sequelize)
     *
     * @memberOf WOrmReladb
     * @param {Object} [option={}] 輸入設定物件,預設{}
     * @param {String} [option.storage='./worm.db'] 輸入sqlite資料庫檔案位置字串,預設'./worm.db'
     * @param {String} [option.db='worm'] 輸入資料庫名稱字串,預設'worm'
     * @param {String} [option.username='username'] 輸入使用者名稱字串,預設'username'
     * @param {String} [option.password='password'] 輸入密碼字串,預設'password'
     * @param {String} [option.dialect='mssql'] 輸入資料庫種類字串,預設'mssql',可選'mssql', 'sqlite', 'mysql', 'mariadb', 'postgres'
     * @param {String} [option.fdModels='./models'] 輸入models儲存的資料夾名稱字串,預設'./models'
     * @param {String} [option.host='localhost'] 輸入連線主機host位址字串,預設'localhost'
     * @param {Integer} [option.port=1433] 輸入連線主機port整數,預設1433
     * @returns {Promise} 回傳Promise,resolve回傳產生的models資料,reject回傳錯誤訊息
     */
    function genModelsByDB(option = {}) {

        //default
        let def = {
            username: 'username',
            password: 'password',
            dialect: 'mssql',
            host: 'localhost',
            port: 1433,
            storage: './worm.db',
        }

        //merge
        option = {
            ...def,
            ...option,
        }

        //database
        if (!option.db) {
            option.db = 'worm'
        }
        option.database = opt.db

        //directory
        if (!option.fdModels) {
            option.fdModels = './models'
        }
        option.directory = opt.fdModels

        //storage
        option.storage = path.resolve(option.storage)

        //WAutoSequelize
        return WAutoSequelize(option)

    }


    /**
     * 由資料表物件生成各資料表的models資料
     *
     * @memberOf WOrmReladb
     * @param {String} [fd='./models'] 輸入models儲存的資料夾名稱字串,預設'./models'
     * @param {Object} [tabs={}] 輸入各資料表物件,預設{}
     * @param {Object} [opt={}] 輸入設定物件,預設{}
     * @param {String} [opt.type='js'] 輸入資料表類型字串,預設'js'
     */
    function genModelsByTabs(...input) {
        _genModelsByTabs(...input)
    }


    //bind
    ee.createStorage = createStorage
    ee.genModelsByDB = genModelsByDB
    ee.genModelsByTabs = genModelsByTabs
    ee.init = initSequelize
    // ee.close = () => {
    //     return closeSequelize('external')
    // }
    ee.genTransaction = genTransaction
    if (opt.useStable) {
        //用佇列(同時最大執行數1且先進先執行)處理高併發之情形
        //若沒管控:
        //mssql就是先執行先贏
        //Sqlite有機會無法儲存, 或是出錯[Error: SQLITE_MISUSE: bad parameter or other API misuse]或[Error: SQLITE_MISUSE: Database handle is closed]
        ee.select = function() {
            return pmq(select, ...arguments)
        }
        ee.selectByPk = function() {
            return pmq(selectByPk, ...arguments)
        }
        ee.insert = function() {
            return pmq(insert, ...arguments)
        }
        ee.insertBulk = function() {
            return pmq(insertBulk, ...arguments)
        }
        ee.save = function() {
            return pmq(save, ...arguments)
        }
        ee.del = function() {
            return pmq(del, ...arguments)
        }
        ee.delAll = function() {
            return pmq(delAll, ...arguments)
        }
    }
    else {
        ee.select = select
        ee.selectByPk = selectByPk
        ee.insert = insert
        ee.insertBulk = insertBulk
        ee.save = save
        ee.del = del
        ee.delAll = delAll
    }

    return ee
}


export default WOrmReladb