import Hapi from '@hapi/hapi'
//import Inert from '@hapi/inert' //提供靜態檔案
import cloneDeep from 'lodash-es/cloneDeep.js'
import keys from 'lodash-es/keys.js'
import get from 'lodash-es/get.js'
import uniqBy from 'lodash-es/uniqBy.js'
import isEqual from 'lodash-es/isEqual.js'
import each from 'lodash-es/each.js'
import genPm from 'wsemi/src/genPm.mjs'
import genID from 'wsemi/src/genID.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import arrHas from 'wsemi/src/arrHas.mjs'
/**
* 建立http API伺服器
*
* @param {Object} opt 輸入設定參數物件
* @param {Object} [opt.serverHapi={}] 輸入hapi伺服器物件,若提供,本服務將自動加入api至route。使用外部hapi伺服器時,需開啟跨域功能,或是使用nginx反向代理轉入api請求
* @param {Integer} [opt.port=8080] 輸入http API伺服器所在port,預設8080
* @param {String} [opt.apiName='api'] 輸入http API伺服器網址的api名稱,預設'api'
* @param {Function} opt.authenticate 輸入使用者身份認證函數,供伺服器端驗證之用,函數會傳入使用者端連線之token參數,回傳為Promise,resolve(true)為驗證通過,resolve(false)為驗證不通過
* @param {Object} [opt.funcs={}] 輸入伺服器端供使用者端呼叫之函數物件,各key為函數名稱,對應value為函數本體。各函數之輸入需為單一物件,而各函數回傳皆為Promise,可通過resolve與reject回傳結果,預設{}
* @param {Array} [opt.routes=[]] 輸入伺服器額外掛載routes陣列,預設[]
* @example
*
* import WComorHapiServer from 'w-comor-hapi/dist/w-comor-hapi-server.umd.js'
*
* function random(min, max) {
* return Math.floor(Math.random() * max) + min
* }
*
* let opt = {
* port: 8080,
* apiName: 'api',
* authenticate: function(token) {
* //使用token驗證使用者身份
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve(true)
* }, 1000)
* })
* },
* filterFuncs: function(token, funcs) {
* //使用token驗證使用者身份與過濾可用funcs
* return new Promise(function(resolve, reject) {
* funcs = funcs.filter(function(v) {
* return v.indexOf('Hide') < 0
* })
* resolve(funcs)
* })
* },
* onClientChange: function(conns, opt) {
* console.log(`Server[port:${opt.port}] now conns: ${conns.length}`)
* },
* funcs: {
* 'group.plus': function({ p1, p2 }) {
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve(p1 * p2)
* }, random(100, 3000))
* })
* },
* 'group.div': function({ p1, p2 }) {
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve(p1 / p2)
* }, random(100, 3000))
* })
* },
* 'add': function({ p1, p2 }) {
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve(p1 + p2)
* }, random(100, 3000))
* })
* },
* 'addHide': function({ p1, p2 }) {
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve(p1 + p2)
* }, random(100, 3000))
* })
* },
* 'minu': function({ p1, p2 }) {
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve(p1 - p2)
* }, random(100, 3000))
* })
* },
* },
* routes: [
* {
* method: 'GET',
* path: '/code',
* handler: function (req, res) {
* //console.log(req)
* return 'get code query: ' + JSON.stringify(req.query)
* //http://localhost:8080/code?a=1&bb=23.45
* // => get code query: {"a":"1","bb":"23.45"}
* }
* },
* ],
* }
*
* new WComorHapiServer(opt)
*
*/
function WComorHapiServer(opt) {
let conns = []
let clients = []
//cloneDeep
opt = cloneDeep(opt)
//default
if (!opt.port) {
opt.port = 8080
}
if (!opt.apiName) {
opt.apiName = 'api'
}
if (!opt.routes) {
opt.routes = []
}
//funcs
let funcs = []
if (haskey(opt, 'funcs')) {
funcs = keys(opt['funcs'])
}
//authenticate
function authenticate(token) {
let pm = genPm()
if (isfun(opt.authenticate)) {
opt.authenticate(token)
.then(function(vd) {
pm.resolve(vd)
})
}
else {
pm.resolve(true)
}
return pm
}
//execFunction
async function execFunction(data) {
//console.log(`Server[port:${opt.port}][api:${opt.apiName}]: `, data)
//token
let token = get(data, 'token', '')
//vd
let vd = await authenticate(token)
//check
if (vd) {
//func
let func = get(data, 'func', '')
//input
let input = get(data, 'input')
//getFuncs
if (func === 'getFuncs') {
if (isfun(opt.filterFuncs)) {
funcs = await opt.filterFuncs(token, funcs)
}
//add output
data['output'] = { sys: 'sys', funcs: funcs }
}
//call
else if (arrHas(funcs, func)) {
//call func in opt.funcs
let output = await opt['funcs'][func](input)
//add output
data['output'] = output
}
else {
//return no func
//add output
data['output'] = { err: `can not find: ${func}` }
}
}
else {
//return no authenticate
//add output
data['output'] = { err: `can not authenticate token: ${token}` }
}
//delete input, 因input可能很大故回傳數據不包含原input
delete data['input']
//return data
return JSON.stringify(data)
}
//changeConns
function changeConns(conns) {
if (isfun(opt.onClientChange)) {
let clients_new = uniqBy(conns, 'clientId')
if (!isEqual(clients, clients_new)) {
clients = clients_new
opt.onClientChange(clients, opt)
}
}
}
//api
let api = {
path: '/' + opt.apiName,
method: 'POST',
options: {
payload: {
maxBytes: 1000 * 1024 * 1024, //1g
timeout: 3 * 60 * 1000, //3分鐘, 注意payload timeout必須小於socket timeout
multipart: true, //hapi 19之後修改multipart預設值為false
},
timeout: {
socket: 5 * 60 * 1000, //5分鐘
},
},
handler: function (req, res) {
let pm = genPm()
//connId
let connId = genID()
//clientId
let clientId = get(req.payload, 'clientId')
//client
let client = {
headers: req.headers,
info: req.info,
}
//push
conns.push({
clientId: clientId,
connId: connId,
data: client,
})
changeConns(conns)
//data
let data = req.payload
//execFunction
execFunction(data)
.then(function(r) {
pm.resolve(r)
})
.catch(function(r) {
pm.reject(r)
})
.finally(function() {
//remove
conns = conns.filter(function(c) {
return c.connId !== connId
})
changeConns(conns)
})
return pm
},
}
async function startServer() {
//server
let server = Hapi.server({
port: opt.port,
//host: 'localhost',
routes: {
cors: true
},
})
//register inert
//await server.register(Inert)
//route api
server.route(api)
//add routes
each(opt.routes, function(v) {
server.route(v)
})
//start
await server.start()
console.log(`Server running at: ${server.info.uri}`)
}
if (opt.serverHapi) {
opt.serverHapi.route(api)
}
else {
startServer()
}
}
export default WComorHapiServer