server/procCore.mjs

import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import size from 'lodash-es/size.js'
import sortBy from 'lodash-es/sortBy.js'
import isestr from 'wsemi/src/isestr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import arrHas from 'wsemi/src/arrHas.mjs'
import ltdtDiffByKey from 'wsemi/src/ltdtDiffByKey.mjs'
import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
import ds from '../src/schema/index.mjs'


/**
 * 僅採納既有列之指定欄位變更, 其餘欄位、新增列、缺列(刪除)與順序一律以資料庫現值為準
 *
 * 用於 modeEditUsers='for:grups': 使用者身分由外部單一登入系統同步, 本系統只擁有群組指派(cgrups), 故 API 層須與 UI 一致只接受 cgrups 變更
 *
 * @param {Array} ltdtDb 資料庫現有列(select 結果, 依 order 排序後為基底)
 * @param {Array} rowsIn 前端/外部送入之整表列
 * @param {String} keyDetect 對應列之鍵欄位(如 'id')
 * @param {Array} pickKeys 允許採納之欄位名陣列(如 ['cgrups'])
 * @returns {Array} 以資料庫列為基底、僅覆蓋 pickKeys 之新列陣列(順序同資料庫)
 */
function mergePickKeysOnly(ltdtDb, rowsIn, keyDetect, pickKeys) {
    let kpIn = {}
    each(rowsIn, (r) => {
        let v = get(r, keyDetect, '')
        if (isestr(v)) {
            kpIn[v] = r
        }
    })
    let ltdt = sortBy(ltdtDb, 'order')
    ltdt = map(ltdt, (u) => {
        let r = { ...u }
        let v = get(u, keyDetect, '')
        let rin = get(kpIn, v, null)
        if (rin !== null) {
            each(pickKeys, (k) => {
                if (haskey(rin, k)) {
                    r[k] = rin[k]
                }
            })
        }
        return r
    })
    return ltdt
}


function proc(woItems, procOrm, { srLog, kmx }) {


    //updateTabItems
    let updateTabItems = async (keyTable, userId, rows, keyDetect, opt = {}) => {
        // console.log('updateTabItems', keyTable, userId, rows.length, keyDetect)

        //pickKeysOnly, 僅採納既有列之指定欄位變更(見 mergePickKeysOnly), 未給則整表 diff 寫入
        let pickKeysOnly = get(opt, 'pickKeysOnly', null)

        //序列化同表之並行整表批次寫入: 防 lost update (同 keyTable 序列化, 不同表並行)
        return await kmx('updateTabItems:' + keyTable, async () => {

        //pickKeysOnly, 須於 mutex 內讀取資料庫為基底, 避免與並行寫入 race
        if (isearr(pickKeysOnly)) {
            let ltdtDb = await woItems[keyTable].select()
            rows = mergePickKeysOnly(ltdtDb, rows, keyDetect, pickKeysOnly)
            srLog.info({ event: 'updateTabItems-pickKeysOnly', keyTable, userId, pickKeysOnly })
        }

        //ltdtmapping
        rows = ltdtmapping(rows, ds[keyTable].keys)
        // console.log('ltdtmapping rows', rows)

        //重給order
        rows = map(rows, (r, k) => {
            r.order = k + 1
            return r
        })

        //ckKey, 回傳 err key (saveRowFieldInvalid / saveRowFieldDuplicate) 或 null; 動態細節 (row index/field/value) 改記 srLog, 不進使用者訊息
        let ckKey = (rows, key) => {
            let err = null

            //check
            let kp = {}
            each(rows, (v, k) => {

                //value
                let value = get(v, key, '')

                //check
                if (!isestr(value)) {
                    srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key, msg: 'saveRowFieldInvalid', row: k })
                    err = `saveRowFieldInvalid`
                    return false //跳出
                }

                //check
                if (haskey(kp, value)) {
                    srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key, msg: 'saveRowFieldDuplicate', row: k, value })
                    err = `saveRowFieldDuplicate`
                    return false //跳出
                }

                //kp
                kp[value] = true

            })

            return err
        }

        //偵測未給予或重複
        let err = null
        if (true) {
            if (arrHas(keyTable, ['targets'])) {
                err = ckKey(rows, 'id')
                if (err !== null) {
                    return Promise.reject(err)
                }
            }
            if (arrHas(keyTable, ['pemis', 'grups'])) { //users可重複name故不列入
                err = ckKey(rows, 'name')
                if (err !== null) {
                    return Promise.reject(err)
                }
            }
            if (arrHas(keyTable, ['users'])) {
                err = ckKey(rows, 'email')
                if (err !== null) {
                    return Promise.reject(err)
                }
            }
        }

        //ltdtDiffByKey
        let ltdtOld = await woItems[keyTable].select()
        let ltdtNew = rows
        let r = ltdtDiffByKey(ltdtOld, ltdtNew, keyDetect)
        // console.log('ltdtDiffByKey r', r)

        //del
        if (size(r.del) > 0) {
            await procOrm(userId, keyTable, 'del', r.del) //須使用procOrm才有辦法自動給予相關欄位
            // .catch((err) => {
            //     console.log('woItems[keyTable].del err', err)
            // })
        }

        //add
        if (size(r.add) > 0) {
            await procOrm(userId, keyTable, 'insert', r.add) //須使用procOrm才有辦法自動給予相關欄位
            // .catch((err) => {
            //     console.log('woItems[keyTable].insert err', err)
            // })
        }

        //diff
        if (size(r.diff) > 0) {
            await procOrm(userId, keyTable, 'save', r.diff) //須使用procOrm才有辦法自動給予相關欄位
            // .catch((err) => {
            //     console.log('woItems[keyTable].save err', err)
            // })
        }

        return ltdtNew

        })
    }


    //errLog: 失敗統一記 err key (updateTabItems reject 為 key 字串如 saveRowFieldInvalid; procOrm 失敗為 Error)
    let errLog = (event, userId, err) => {
        srLog.error({ event, userId, msg: (typeof err === 'string') ? err : ((err && err.message) || event) })
    }


    //updateTargets
    let updateTargets = async (userId, rows) => {
        try {
            await updateTabItems('targets', userId, rows, 'id')
        }
        catch (err) { errLog('updateTargets-error', userId, err); throw err }
        srLog.info({ event: 'updateTargets-success', userId })
        return 'saveTabItemsSuccess' //成功 msg key
    }


    //updatePemis
    let updatePemis = async (userId, rows) => {
        try {
            await updateTabItems('pemis', userId, rows, 'name')
        }
        catch (err) { errLog('updatePemis-error', userId, err); throw err }
        srLog.info({ event: 'updatePemis-success', userId })
        return 'saveTabItemsSuccess' //成功 msg key
    }


    //updateGrups
    let updateGrups = async (userId, rows) => {
        try {
            await updateTabItems('grups', userId, rows, 'name')
        }
        catch (err) { errLog('updateGrups-error', userId, err); throw err }
        srLog.info({ event: 'updateGrups-success', userId })
        return 'saveTabItemsSuccess' //成功 msg key
    }


    //updateUsers, opt.pickKeysOnly 給 ['cgrups'] 時僅採納既有使用者之群組指派變更(modeEditUsers='for:grups')
    let updateUsers = async (userId, rows, opt = {}) => {
        try {
            await updateTabItems('users', userId, rows, 'id', opt)
        }
        catch (err) { errLog('updateUsers-error', userId, err); throw err }
        srLog.info({ event: 'updateUsers-success', userId })
        return 'saveTabItemsSuccess' //成功 msg key
    }


    let p = {
        updateTargets,
        updatePemis,
        updateGrups,
        updateUsers,
    }


    return p
}


export { mergePickKeysOnly }


export default proc