WServWebdataServer.mjs

import get from 'lodash-es/get.js'
import evem from 'wsemi/src/evem.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import now2str from 'wsemi/src/now2str.mjs'
import genID from 'wsemi/src/genID.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import WSyncWebdataServer from 'w-sync-webdata/src/WSyncWebdataServer.mjs'
import WServBroadcastServer from 'w-serv-broadcast/src/WServBroadcastServer.mjs'
import pickTables from './pickTables.mjs'
import WServWebdataServerSync from './WServWebdataServerSync.mjs'
import WServWebdataServerExec from './WServWebdataServerExec.mjs'


/**
 * 伺服器端之資料控制與同步器
 *
 * @class
 * @param {Object} instWConverServer 輸入通訊服務實體物件,可使用例如WConverhpServer等建立
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.useDbOrm=true] 輸入是否使用資料庫ORM技術,給予false代表不使用直接存取資料庫函數與自動同步資料庫至前端功能,預設true
 * @param {Object} [opt.kpOrm={}] 輸入各資料表的操作物件,用以提供由tableNamesSync指定資料表的change事件,使能監聽與觸發資料變更事件,key為表名而值為該表的操作器實體,操作器實體可使用例如WOrmMongodb等建立,預設{}
 * @param {Function} [opt.operOrm={}] 輸入各資料表的操作通用接口物件,用以提供操作由tableNamesExec指定資料表的例如'select'、'insert'、'save'、'del'函數。加上由kpFunExt提供的函數,就為全部可由前端執行的函數,預設{}
 * @param {Array} [opt.tableNamesExec=[]] 輸入指定能被操作的表名陣列,預設[]
 * @param {Array} [opt.tableNamesSync=[]] 輸入指定能被同步的表名陣列,預設[]
 * @param {Array} [opt.methodsExec=['select','insert','save','del']] 輸入指定綁定操作器的方式陣列,可選'select'、'insert'、'save'、'del'、'delAll',預設['select', 'insert', 'save', 'del']
 * @param {Function} [opt.getUserIdByToken=async()=>''] 輸入取得使用者ID的回調函數,傳入參數為各函數的原始參數,預設async()=>''
 * @param {Object} [opt.kpFunExt=null] 輸入額外擴充執行函數物件,key為函數名而值為函數,預設null
 * @param {String} [opt.fpTableTags='tableTags.json'] 輸入儲存各資料表時間戳檔案路徑串,預設'./tableTags.json'
 * @param {Function} [opt.genTag=()=>'{random string}'] 輸入產生不重複識別碼函數,預設()=>'{random string}'
 * @returns {Object} 回傳事件物件,可監聽error事件
 * @example
 *
 * // import fs from 'fs'
 * import _ from 'lodash-es'
 * import WConverhpServer from 'w-converhp/src/WConverhpServer.mjs'
 * // import WOrm from 'w-orm-mongodb/src/WOrmMongodb.mjs' //自行選擇引用ORM
 * import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs' //自行選擇引用ORM
 * import WServWebdataServer from './src/WServWebdataServer.mjs'
 *
 * let ms = []
 *
 * //預先刪除w-orm-lowdb資料庫
 * try {
 *     fs.unlinkSync('./db.json')
 * }
 * catch (err) {}
 *
 * //optWOrm
 * let optWOrm = {
 *     // url: 'mongodb://username:password@127.0.0.1:27017',
 *     // db: 'servdata',
 *     url: './db.json',
 *     db: 'servdata',
 *     cl: '',
 * }
 *
 * //tableNamesExec, tableNamesSync
 * let tableNamesExec = ['tabA', 'tabB']
 * let tableNamesSync = ['tabA']
 *
 * //kpOrm
 * let kpOrm = {}
 * for (let k in tableNamesExec) {
 *     let v = tableNamesExec[k]
 *     let opt = { ...optWOrm, cl: v }
 *     let wo = new WOrm(opt)
 *     kpOrm[v] = wo
 * }
 * // console.log('kpOrm', kpOrm)
 *
 * //saveData
 * let saveData = async(cl, r) => {
 *
 *     //w
 *     let w = kpOrm[cl] //一定要由kpOrm操作, 否則傳kpOrm進去WServWebdataServer會無法收到change事件
 *     console.log('saveData cl', cl, r)
 *     ms.push({ 'saveData before': { cl, data: JSON.stringify(r) } })
 *
 *     //save
 *     await w.save(r, { autoInsert: true })
 *         .then(function(msg) {
 *             console.log('save then', cl, msg)
 *             ms.push({ 'saveData after': { cl, data: JSON.stringify(msg) } })
 *         })
 *         .catch(function(err) {
 *             console.log('save catch', cl, err)
 *         })
 *
 * }
 *
 * let r
 *
 * r = [
 *     {
 *         id: 'id-tabA-peter',
 *         name: 'peter',
 *         value: 123,
 *     },
 *     {
 *         id: 'id-tabA-rosemary',
 *         name: 'rosemary',
 *         value: 123.456,
 *     },
 *     {
 *         id: 'id-tabA-kettle',
 *         name: 'kettle',
 *         value: 456,
 *     },
 * ]
 *
 * await saveData('tabA', r)
 * console.log('saveData tabA')
 *
 * r = [
 *     {
 *         id: 'id-tabB-peter',
 *         name: 'peter',
 *         value: 123,
 *     },
 *     {
 *         id: 'id-tabB-rosemary',
 *         name: 'rosemary',
 *         value: 123.456,
 *     },
 * ]
 *
 * await saveData('tabB', r)
 * console.log('saveData tabB')
 *
 * let n = 0
 * let t = setInterval(async () => {
 *     n++
 *     console.log('update tabA', n)
 *     r = {
 *         id: 'id-tabA-peter',
 *         name: 'peter',
 *         value: `peter-n[${n}]`,
 *     }
 *     ms.push({ 'timer update tabA before': n, r })
 *     await saveData('tabA', r)
 *     ms.push({ 'timer update tabA after': n, r })
 *     if (n >= 5) {
 *         clearInterval(t)
 *     }
 * }, 2000)
 *
 * let instWConverServer = new WConverhpServer({
 *     port: 9000,
 * })
 *
 * let procCommon = async (userId, tableName, methodName, input) => {
 *     // console.log('procCommon call', tableName, methodName, input)
 *     let r = await kpOrm[tableName][methodName](input)
 *     // console.log('procCommon result', r)
 *     return r
 * }
 *
 * instWConverServer = new WServWebdataServer(instWConverServer, {
 *     getUserIdByToken: async (token) => { //可使用async或sync函數
 *         return 'id-for-admin'
 *     },
 *     kpOrm,
 *     operOrm: procCommon, //procCommon的輸入為: userId, tableName, methodName, input
 *     tableNamesExec,
 *     tableNamesSync,
 *     kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
 *         uploadFile: async (userId, { name, u8a }) => {
 *             console.log('uploadFile', userId, name, _.size(u8a))
 *             ms.push({ 'uploadFile before': { name, size: _.size(u8a) } })
 *             // fs.writeFileSync(name, Buffer.from(u8a))
 *             ms.push({ 'uploadFile after': { name, size: _.size(u8a) } })
 *             console.log('uploadFile writeFileSync finish')
 *             return { name, size: _.size(u8a) }
 *         },
 *         add: (userId, input) => {
 *             console.log('add', input)
 *             let r = input.pa + input.pb
 *             ms.push({ 'kpFunExt add': { input: JSON.stringify(input), output: JSON.stringify(r) } })
 *             return r
 *         },
 *         //...
 *     },
 *     // fpTableTags: 'tableTags-serv-webdata.json',
 * })
 *
 * //error
 * instWConverServer.on('error', (err) => {
 *     console.log('error', err)
 * })
 *
 * //sync會通過broadcast給前端還需要時間處理, 故不能於滿足條件就stop
 * setTimeout(() => {
 *     instWConverServer.clearBroadcast()
 *     instWConverServer.stop()
 *     console.log('ms', ms)
 * }, 14000)
 * // => ms [
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabA',
 * //       data: '[{"id":"id-tabA-peter","name":"peter","value":123},{"id":"id-tabA-rosemary","name":"rosemary","value":123.456},{"id":"id-tabA-kettle","name":"kettle","value":456}]'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': {
 * //       cl: 'tabA',
 * //       data: '[{"n":1,"nInserted":1,"ok":1},{"n":1,"nInserted":1,"ok":1},{"n":1,"nInserted":1,"ok":1}]'
 * //     }
 * //   },
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabB',
 * //       data: '[{"id":"id-tabB-peter","name":"peter","value":123},{"id":"id-tabB-rosemary","name":"rosemary","value":123.456}]'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': {
 * //       cl: 'tabB',
 * //       data: '[{"n":1,"nInserted":1,"ok":1},{"n":1,"nInserted":1,"ok":1}]'
 * //     }
 * //   },
 * //   {
 * //     'timer update tabA before': 1,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[1]' }
 * //   },
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabA',
 * //       data: '{"id":"id-tabA-peter","name":"peter","value":"peter-n[1]"}'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': { cl: 'tabA', data: '[{"n":1,"nModified":1,"ok":1}]' }
 * //   },
 * //   {
 * //     'timer update tabA after': 1,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[1]' }
 * //   },
 * //   {
 * //     'timer update tabA before': 2,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[2]' }
 * //   },
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabA',
 * //       data: '{"id":"id-tabA-peter","name":"peter","value":"peter-n[2]"}'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': { cl: 'tabA', data: '[{"n":1,"nModified":1,"ok":1}]' }
 * //   },
 * //   {
 * //     'timer update tabA after': 2,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[2]' }
 * //   },
 * //   {
 * //     'timer update tabA before': 3,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[3]' }
 * //   },
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabA',
 * //       data: '{"id":"id-tabA-peter","name":"peter","value":"peter-n[3]"}'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': { cl: 'tabA', data: '[{"n":1,"nModified":1,"ok":1}]' }
 * //   },
 * //   {
 * //     'timer update tabA after': 3,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[3]' }
 * //   },
 * //   {
 * //     'timer update tabA before': 4,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[4]' }
 * //   },
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabA',
 * //       data: '{"id":"id-tabA-peter","name":"peter","value":"peter-n[4]"}'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': { cl: 'tabA', data: '[{"n":1,"nModified":1,"ok":1}]' }
 * //   },
 * //   {
 * //     'timer update tabA after': 4,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[4]' }
 * //   },
 * //   {
 * //     'timer update tabA before': 5,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[5]' }
 * //   },
 * //   {
 * //     'saveData before': {
 * //       cl: 'tabA',
 * //       data: '{"id":"id-tabA-peter","name":"peter","value":"peter-n[5]"}'
 * //     }
 * //   },
 * //   {
 * //     'saveData after': { cl: 'tabA', data: '[{"n":1,"nModified":1,"ok":1}]' }
 * //   },
 * //   {
 * //     'timer update tabA after': 5,
 * //     r: { id: 'id-tabA-peter', name: 'peter', value: 'peter-n[5]' }
 * //   }
 * // ]
 *
 */
function WServWebdataServer(instWConverServer, opt = {}) {

    //check
    if (!iseobj(instWConverServer)) {
        console.log('instWConverServer is not an effective object, and set instWConverServer to an EventEmitter')
        instWConverServer = evem()
    }
    if (!haskey(instWConverServer, 'emit')) {
        throw new Error(`instWConverServer is not an EventEmitter`)
    }

    //fpTableTags
    let fpTableTags = get(opt, 'fpTableTags', null)
    if (!isestr(fpTableTags)) {
        fpTableTags = './tableTags.json'
    }

    //genTag
    let genTag = get(opt, 'genTag')
    if (!isfun(genTag)) {
        genTag = () => {
            return now2str() + '|' + genID(6)
        }
    }

    //擴充同步資料功能
    instWConverServer = new WSyncWebdataServer(instWConverServer, {
        fpTableTags,
        genTag, //供updateTableTag使用
    })

    //擴充廣播功能
    instWConverServer = new WServBroadcastServer(instWConverServer)

    //useDbOrm
    let useDbOrm = get(opt, 'useDbOrm', null)
    if (!isbol(useDbOrm)) {
        useDbOrm = true
    }

    //kpOrm, 輸入外部ORM實體
    let kpOrm = get(opt, 'kpOrm', null)

    //operOrm, ORM的泛用接口procCommon
    let operOrm = get(opt, 'operOrm', null)

    //tableNamesExec, 指定能被呼叫的表名, 各表名需隸屬於ORM, 才能被ORM的泛用接口procCommon處理
    let tableNamesExec = get(opt, 'tableNamesExec', null)

    //methodsExec
    let methodsExec = get(opt, 'methodsExec', null)

    //tableNamesSync, 指定需同步表名, 機敏資訊表記得過濾, 各表名需隸屬於ORM, 才能監聽其change事件
    let tableNamesSync = get(opt, 'tableNamesSync', null)

    //getUserIdByToken, 由外部提供呼叫函數, 將提供各函數的輸入用以取得使用userID
    let getUserIdByToken = get(opt, 'getUserIdByToken', null)

    //kpFunExt
    let kpFunExt = get(opt, 'kpFunExt', null)
    if (!iseobj(kpFunExt)) {
        kpFunExt = {}
    }

    //提供同步資料[sys:getTableTags]函數
    kpFunExt = {
        ...kpFunExt,
        '[sys:getTableTags]': () => {
            let nowTableTags = instWConverServer.getTableTags()
            // console.log('nowTableTags', nowTableTags)
            return {
                mode: 'syncKpTable',
                data: pickTables(tableNamesSync, nowTableTags),
            }
        },
    }

    //WServWebdataServerExec
    instWConverServer = new WServWebdataServerExec(
        instWConverServer,
        {
            getUserIdByToken,
            useDbOrm,
            operOrm,
            tableNames: tableNamesExec,
            methods: methodsExec,
            kpFunExt,
        },
    )

    //useDbOrm
    if (useDbOrm) {

        //check
        if (!isobj(kpOrm)) {
            instWConverServer.emit('error', 'invalid opt.kpOrm when useDbOrm=true')
            return instWConverServer
        }

        //WServWebdataServerSync
        instWConverServer = new WServWebdataServerSync(
            instWConverServer,
            tableNamesSync,
            kpOrm,
            {
                genTag, //供初始化tableTagsSrv用
            },
        )

    }

    return instWConverServer
}


export default WServWebdataServer