WServHapiServer.mjs

import Hapi from '@hapi/hapi'
import Inert from '@hapi/inert'
import get from 'lodash-es/get.js'
import now2str from 'wsemi/src/now2str.mjs'
import genID from 'wsemi/src/genID.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isint from 'wsemi/src/isint.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import evem from 'wsemi/src/evem.mjs'
import waitFun from 'wsemi/src/waitFun.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
import WConverhpServer from 'w-converhp/src/WConverhpServer.mjs'
import WServWebdataServer from 'w-serv-webdata/src/WServWebdataServer.mjs'


/**
 * Hapi伺服器端之資料控制與同步器
 *
 * @class
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Integer} [opt.port=8080] 輸入Hapi伺服器通訊port,預設8080
 * @param {Array} [opt.corsOrigins=['http://localhost']] 輸入允許跨域呼叫之網域陣列,給予['*']代表允許外部任意網域跨域,預設['http://localhost']
 * @param {Boolean} [opt.useInert=true] 輸入是否使用@hapi/inert取得指定資料夾下所有靜態檔案,預設true
 * @param {String} [opt.pathStaticFiles='dist'] 輸入當useInert=true時指定伺服器資料夾名稱,預設'dist'
 * @param {Array} [opt.apis=[]] 輸入Hapi伺服器設定API陣列,預設[]
 * @param {String} [opt.apiName='api'] 輸入API名稱字串,預設'api'
 * @param {String} [opt.pathUploadTemp='./uploadTemp'] 輸入暫時存放切片上傳檔案資料夾字串,預設'./uploadTemp'
 * @param {String} [opt.tokenType='Bearer'] 輸入token類型字串,預設'Bearer'
 * @param {Integer} [opt.sizeSlice=1024*1024] 輸入切片上傳檔案之切片檔案大小整數,單位為Byte,預設為1024*1024
 * @param {Function} [opt.verifyConn=()=>{return true}] 輸入呼叫API時檢測函數,預設()=>{return true}
 * @param {Function} [opt.getUserIdByToken=async()=>''] 輸入取得使用者ID的回調函數,傳入參數為各函數的原始參數,預設async()=>''
 * @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'函數。加上由extFuncs提供的函數,就為全部可由前端執行的函數,預設{}
 * @param {Array} [opt.tableNamesExec=[]] 輸入指定能被操作的表名陣列,預設[]
 * @param {Array} [opt.methodsExec=['select','insert','save','del']] 輸入指定綁定操作器的方式陣列,可選'select'、'insert'、'save'、'del'、'delAll',預設['select', 'insert', 'save', 'del']
 * @param {Array} [opt.tableNamesSync=[]] 輸入指定能被同步的表名陣列,預設[]
 * @param {Object} [opt.kpFunExt=null] 輸入額外擴充執行函數物件,key為函數名而值為函數,預設null
 * @param {String} [opt.fpTableTags='tableTags.json'] 輸入儲存各資料表時間戳檔案路徑串,預設'./tableTags.json'
 * @param {Function} [opt.genTag=()=>'{random string}'] 輸入產生不重複識別碼函數,預設()=>'{random string}'
 * @param {Boolean} [opt.showLog=true] 輸入是否使用console.log顯示基本資訊布林值,預設true
 * @returns {Object} 回傳事件物件,提供getServer函數回傳hapi伺服器實體,提供getInstWConverServer回傳擴展功能實體,可監聽error事件
 * @example
 *
 * import fs from 'fs'
 * import _ from 'lodash-es'
 * // import WOrm from 'w-orm-mongodb/src/WOrmMongodb.mjs' //自行選擇引用ORM
 * import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs' //自行選擇引用ORM
 * import WServHapiServer from './src/WServHapiServer.mjs'
 *
 * async function run() {
 *
 *     //預先刪除w-orm-lowdb資料庫
 *     try {
 *         fs.unlinkSync('./db.json')
 *     }
 *     catch (err) {}
 *
 *     //optWOrm
 *     let optWOrm = {
 *         // url: 'mongodb://username:password@127.0.0.1:27017',
 *         // db: 'servhapi',
 *         url: './db.json',
 *         db: 'servhapi',
 *         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
 *     }
 *
 *     async function saveData(cl, r) {
 *
 *         //w
 *         let w = kpOrm[cl] //一定要由woItems操作, 否則傳woItems進去WServHapiServer會無法收到change事件
 *
 *         //save
 *         await w.save(r) //autoInsert: false
 *             .then(function(msg) {
 *                 console.log('save then', cl, msg)
 *             })
 *             .catch(function(msg) {
 *                 console.log('save catch', cl, msg)
 *             })
 *
 *     }
 *
 *     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)
 *     r = [
 *         {
 *             id: 'id-tabB-peter',
 *             name: 'peter',
 *             value: 123,
 *         },
 *         {
 *             id: 'id-tabB-rosemary',
 *             name: 'rosemary',
 *             value: 123.456,
 *         },
 *     ]
 *     await saveData('tabB', r)
 *
 *     let ntv = 0
 *     let genValue = () => {
 *         ntv++
 *         return `value-${ntv}`
 *     }
 *
 *     let n = 0
 *     let tn = setInterval(() => {
 *         n++
 *         console.log('update tabA', n)
 *         r = {
 *             id: 'id-tabA-peter',
 *             name: 'peter',
 *             value: genValue(),
 *         }
 *         saveData('tabA', r)
 *         if (n >= 5) {
 *             clearInterval(tn)
 *         }
 *     }, 3000)
 *
 *     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
 *     }
 *
 *     let uploadFile = async (userId, { name, u8a }) => {
 *         console.log('uploadFile', userId, name, _.size(u8a))
 *         // fs.writeFileSync(name, Buffer.from(u8a))
 *         console.log('uploadFile writeFileSync finish')
 *         return 'finish'
 *     }
 *
 *     let add = async (userId, { pa, pb }) => {
 *         // console.log('add', userId, pa, pb)
 *         return `result: pa+pb=${pa + pb}`
 *     }
 *
 *     let ntg = 0
 *     let genTag = () => {
 *         ntg++
 *         return `tag-${ntg}`
 *     }
 *
 *     //WServHapiServer
 *     let wsrv = new WServHapiServer({
 *         port: 8080,
 *         apis: [],
 *         verifyConn: ({ apiType, authorization, headers, query }) => {
 *             console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
 *             return true
 *         },
 *         getUserIdByToken: async (token) => { //可使用async或sync函數
 *             return 'id-for-admin'
 *         },
 *         useDbOrm: true,
 *         kpOrm,
 *         operOrm: procCommon, //procCommon的輸入為: userId, tableName, methodName, input
 *         tableNamesExec,
 *         methodsExec: ['select', 'insert', 'save', 'del'],
 *         tableNamesSync,
 *         kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
 *             uploadFile,
 *             add,
 *             //...
 *         },
 *         // fnTableTags: 'tableTags-serv-hapi.json',
 *         genTag,
 *     })
 *
 *     wsrv.on('error', (err) => {
 *         console.log(err)
 *     })
 *
 * }
 * run()
 *
 * // save then tabA [
 * //   { n: 1, nInserted: 1, ok: 1 },
 * //   { n: 1, nInserted: 1, ok: 1 },
 * //   { n: 1, nInserted: 1, ok: 1 }
 * // ]
 * // save then tabB [ { n: 1, nInserted: 1, ok: 1 }, { n: 1, nInserted: 1, ok: 1 } ]
 * // Server running at: http://DESKTOP-6R7USAO:8080
 * // Server[port:8080]: now clients: 1
 * // uploadFile id-for-admin zdata.b1 3
 * // uploadFile writeFileSync finish
 * // update tabA 1
 * // save then tabA [ { n: 1, nModified: 1, ok: 1 } ]
 * // update tabA 2
 * // save then tabA [ { n: 1, nModified: 1, ok: 1 } ]
 * // update tabA 3
 * // save then tabA [ { n: 1, nModified: 1, ok: 1 } ]
 * // update tabA 4
 * // save then tabA [ { n: 1, nModified: 1, ok: 1 } ]
 * // update tabA 5
 * // save then tabA [ { n: 1, nModified: 1, ok: 1 } ]
 *
 */
function WServHapiServer(opt = {}) {
    let ev = evem()
    let server = null
    let instWConverServer = null

    //port
    let port = get(opt, 'port')
    if (!isint(port)) {
        port = 8080
    }

    //corsOrigins
    let corsOrigins = get(opt, 'corsOrigins', [])
    if (!isearr(corsOrigins)) {
        corsOrigins = ['http://localhost'] //['*']
    }

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

    //pathStaticFiles
    let pathStaticFiles = get(opt, 'pathStaticFiles')
    if (!isestr(pathStaticFiles)) {
        pathStaticFiles = 'dist'
    }

    //apis
    let apis = get(opt, 'apis')
    if (!isearr(apis)) {
        apis = []
    }

    //apiName
    let apiName = get(opt, 'apiName')
    if (!isestr(apiName)) {
        apiName = 'api'
    }

    //pathUploadTemp
    let pathUploadTemp = get(opt, 'pathUploadTemp')
    if (!isestr(pathUploadTemp)) {
        pathUploadTemp = './uploadTemp'
    }
    if (!fsIsFolder(pathUploadTemp)) {
        fsCreateFolder(pathUploadTemp)
    }

    //tokenType
    let tokenType = get(opt, 'tokenType')
    if (!isestr(tokenType)) {
        tokenType = 'Bearer'
    }

    //sizeSlice
    let sizeSlice = get(opt, 'sizeSlice')
    if (!ispint(sizeSlice)) {
        sizeSlice = 1024 * 1024 //1m
    }

    //verifyConn
    let verifyConn = get(opt, 'verifyConn')
    if (!isfun(verifyConn)) {
        verifyConn = () => {
            return true
        }
    }

    //getUserIdByToken
    let getUserIdByToken = get(opt, 'getUserIdByToken', null)
    // if (!isfun(getUserIdByToken)) {
    //     getUserIdByToken = async () => {
    //         return ''
    //     }
    // }

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

    //kpOrm, 輸入外部ORM實體, 常用變數名: kpOrm
    let kpOrm = get(opt, 'kpOrm', null)
    // if (!isobj(kpOrm)) {
    //     ev.emit('error', 'invalid opt.kpOrm')
    //     return ev
    // }

    //operOrm, ORM的泛用接口, 常用變數名: procCommon
    let operOrm = get(opt, 'operOrm', null)
    // if (!isfun(operOrm)) {
    //     ev.emit('error', 'invalid opt.operOrm')
    //     return ev
    // }

    //tableNamesExec, 指定能被呼叫的表名, 各表名需隸屬於ORM, 才能被ORM的泛用接口procCommon處理
    let tableNamesExec = get(opt, 'tableNamesExec', null)
    // if (!isarr(tableNamesExec)) {
    //     ev.emit('error', 'invalid opt.tableNamesExec')
    //     return ev
    // }

    //methodsExec
    let methodsExec = get(opt, 'methodsExec', null)
    // if (!isarr(methodsExec)) {
    //     methodsExec = ['select', 'insert', 'save', 'del']
    // }

    //tableNamesSync, 指定需同步表名, 機敏資訊表記得過濾, 各表名需隸屬於ORM, 才能監聽其change事件
    let tableNamesSync = get(opt, 'tableNamesSync', null)
    // if (!isarr(tableNamesSync)) {
    //     ev.emit('error', 'invalid opt.tableNamesSync')
    //     return ev
    // }

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

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

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

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

    //createHapiServer
    let createHapiServer = async () => {

        //Hapi.Server
        server = new Hapi.Server({
            port,
            routes: {
                timeout: {
                    server: false, //關閉伺服器超時
                    socket: false, //關閉socket超時
                },
                cors: {
                    origin: corsOrigins, //Access-Control-Allow-Origin
                    credentials: false, //Access-Control-Allow-Credentials
                },
            },
        })

        //useInert
        if (useInert) {

            //register inert
            await server.register(Inert)

        }

        //apiRoutes
        let apiRoutes = []
        if (useInert) {
            apiRoutes = [
                {
                    method: 'GET',
                    path: '/{file*}',
                    handler: {
                        directory: {
                            path: `${pathStaticFiles}/`
                        }
                    },
                },
                // {
                //     method: 'GET',
                //     path: '/api/someAPI',
                //     handler: async function (req, res) {

                //         // //token
                //         // let token = _.get(req, 'query.token', '')

                //         return 'someAPI'
                //     },
                // },
            ]
        }
        apiRoutes = [
            ...apiRoutes,
            ...apis,
        ]

        //route apiRoutes
        server.route(apiRoutes)

    }

    //createWConverhpServer
    let createWConverhpServer = async () => {

        //WConverhpServer
        instWConverServer = new WConverhpServer({
            serverHapi: server,
            apiName,
            pathUploadTemp,
            tokenType,
            sizeSlice,
            verifyConn,
            corsOrigins,
        })
        instWConverServer.on('open', function() {
            if (showLog) {
                console.log(`Server[port:${port}]: open`)
            }

            // //broadcast
            // let n = 0
            // setInterval(() => {
            //     n += 1
            //     let o = {
            //         text: `server broadcast hi(${n})`,
            //         data: new Uint8Array([66, 97, 115]), //support Uint8Array data
            //     }
            //     instWConverServer.broadcast(o, function (prog) {
            //         console.log('broadcast prog', prog)
            //     })
            // }, 1000)

        })
        // instWConverServer.on('clientEnter', function(clientId, data) {
        //     console.log(`Server[port:${port}]: client enter: ${clientId}`)
        // })
        instWConverServer.on('clientChange', function(num) {
            if (showLog) {
                console.log(`Server[port:${port}]: now clients: ${num}`)
            }
        })
        // instWConverServer.on('execute', async function(func, input, pm) {
        //     //console.log(`Server[port:${port}]: execute`, func, input)
        //     execute({ func, input, pm })
        // })
        // instWConverServer.on('broadcast', function(data) {
        //     console.log(`Server[port:${port}]: broadcast`, data)
        // })

    }

    //createWServWebdataServer
    let createWServWebdataServer = async () => {

        // //tableNamesExec, tableNamesSync
        // let tableNamesExec = keys(ds)
        // let tableNamesSync = filter(tableNamesExec, (v) => {
        //     return strright(v, 5) !== 'Items'//不同步數據
        // })

        //WServWebdataServer
        instWConverServer = new WServWebdataServer(
            instWConverServer,
            {

                getUserIdByToken,
                // getUserIdByToken: (token) => {
                //     return 'id-for-admin'
                // },

                useDbOrm,
                kpOrm,
                operOrm, //funORMProc的輸入為: userId, tableName, methodName, input
                // operOrm: procCommon, //funORMProc的輸入為: userId, tableName, methodName, input

                tableNamesExec,
                tableNamesSync,

                methodsExec, //methodsExec可提供select, insert, save, del, mix, mix需於procCommon內註冊以提供
                // methodsExec: ['select', 'insert', 'save', 'del', 'mix'], //mix需於procCommon內註冊以提供

                kpFunExt, //註冊擴充函數
                // kpFunExt: {
                //     getUserById,
                //     // downloadAnalysisResultByKey,
                // },

                fnTableTags,
                genTag,

            })

        //共用instWConverServer, 故最後再監聽error與用ev.emit至外部
        instWConverServer.on('error', (err) => {
            ev.emit('error', err)
        })

    }

    //create
    let create = async () => {

        //createHapiServer
        await createHapiServer()

        //createWConverhpServer
        await createWConverhpServer()

        //createWServWebdataServer
        await createWServWebdataServer()

        //start
        await server.start()
        if (showLog) {
            console.log(`Server running at: ${server.info.uri}`)
        }

    }
    create()
        .catch((err) => {
            ev.emit('error', err) //用ev.emit至外部
        })

    //攔截非預期錯誤
    process.on('unhandledRejection', (err) => {
        console.log('unhandledRejection', err) //強制顯示
        ev.emit('error', { mode: 'unhandledRejection', err }) //用ev.emit至外部
    })
    process.on('uncaughtException', (err) => {
        console.log('uncaughtException', err) //強制顯示
        ev.emit('error', { mode: 'uncaughtException', err }) //用ev.emit至外部
    })

    //getServer
    let getServer = async () => {
        await waitFun(() => {
            return server !== null
        })
        return server
    }

    //getInstWConverServer
    let getInstWConverServer = async () => {
        await waitFun(() => {
            return instWConverServer !== null
        })
        return instWConverServer
    }

    //save
    ev.getServer = getServer
    ev.getInstWConverServer = getInstWConverServer

    return ev
}


export default WServHapiServer