import path from 'path'
import events from 'events'
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'
import { Query } from 'mingo'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import merge from 'lodash-es/merge.js'
import isEqual from 'lodash-es/isEqual.js'
import size from 'lodash-es/size.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import genID from 'wsemi/src/genID.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import arrPullAt from 'wsemi/src/arrPullAt.mjs'
import waitFun from 'wsemi/src/waitFun.mjs'
//kpGlock
let kpGlock = {}
/**
* 操作資料庫(lowdb)
*
* @class
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.url='./db.json'] 輸入資料庫位置字串,預設'./db.json'
* @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
* @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
* @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
* @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
*/
function WOrmLowdb(opt = {}) {
//_cache
let _cache = null
//url
let url = get(opt, 'url')
if (!isestr(url)) {
url = './db.json'
}
//db
let db = get(opt, 'db')
if (!isestr(db)) {
db = 'worm'
}
//cl
let cl = get(opt, 'cl')
if (!isestr(cl)) {
cl = 'test'
}
//useCache
let useCache = get(opt, 'useCache')
if (!isbol(useCache)) {
useCache = false
}
//key
let key = `${db}:${cl}`
//adapter
let adapter = new JSONFile(url)
//lowdb
let lowdb = new Low(adapter, {})
// //default, 此處偵測會失效, 因操作函數內會通過lowdb.read()初始化, 預設創建空陣列會被取代
// if (!haskey(lowdb.data, key)) {
// lowdb.data[key] = []
// }
//ee
let ee = new events.EventEmitter()
//gkey, 基於url也就是以檔案路徑為準, 同檔案路徑須使用全域鎖
let gkey = path.resolve(url)
//全域鎖初始化, 一次僅能執行一種操作函數, 使各類操作url(db.json)時能為獨占模式
//select理論上是不用鎖, 但因為操作上db.jon.tmp更名回db.json時似乎非rename而是串流寫入, 導致有機會撈到不完整json數據進而出錯, 故還是得要上鎖以策安全
if (!haskey(kpGlock, gkey)) {
kpGlock[gkey] = false
}
//getData
let getData = async() => {
//check
if (useCache && isarr(_cache)) {
return cloneDeep(_cache) //與外部使用數據脫勾
}
//read
await lowdb.read()
//default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
if (!haskey(lowdb.data, key)) {
lowdb.data[key] = []
}
//ltdt
let ltdt = cloneDeep(lowdb.data[key]) //數據為lowdb內部的記憶體狀態本體, 須cloneDeep與外部使用數據脫勾
if (useCache) {
_cache = ltdt
return cloneDeep(ltdt) //與外部使用數據脫勾
}
return ltdt
}
/**
* 查詢數據
*
* @memberOf WOrmLowdb
* @param {Object} [find={}] 輸入查詢條件物件
* @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
*/
async function selectCore(find = {}) {
let isErr = false
//res
let res = null
try {
//ltdt
let ltdt = await getData()
//filter
if (iseobj(find)) {
//q
let q = new Query(find)
// console.log('q', q)
//find
res = q.find(ltdt).all()
// console.log('res', res)
}
else {
res = ltdt
}
}
catch (err) {
isErr = true
res = err
}
if (isErr) {
return Promise.reject(res)
}
return res
}
async function select(find = {}) {
if (kpGlock[gkey]) {
await waitFun(() => {
return !kpGlock[gkey]
})
}
kpGlock[gkey] = true
return await selectCore(find)
.finally(() => {
kpGlock[gkey] = false
})
}
/**
* 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
*
* @memberOf WOrmLowdb
* @param {Object|Array} data 輸入數據物件或陣列
* @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
*/
async function insertCore(data) {
let isErr = false
//check
if (!iseobj(data) && !isearr(data)) {
return {
n: 0,
nInserted: 0,
ok: 1,
}
}
//cloneDeep, 與外部數據脫勾
data = cloneDeep(data)
//res
let res = null
try {
//check
if (!isarr(data)) {
data = [data]
}
//check id
data = map(data, function(v) {
if (!isestr(v.id)) {
v.id = genID()
}
return v
})
//read
await lowdb.read()
//default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
if (!haskey(lowdb.data, key)) {
lowdb.data[key] = []
}
//kp
let kp = {}
each(lowdb.data[key], (v, k) => {
kp[v.id] = { k, v }
})
//each
let nAll = size(data)
let nPush = 0
let b = false
each(data, (v) => {
//check
if (!haskey(kp, v.id)) {
//未存在v.id
//push
lowdb.data[key].push(v)
nPush++
b = true
}
else {
//已存在v.id則不push
}
})
//write
if (b) {
await lowdb.write()
}
//res
res = {
n: nAll,
nInserted: nPush,
ok: 1,
}
}
catch (err) {
isErr = true
res = err
}
//update, 不能保證插入多少, 一律重設快取
_cache = null
//emit, 於change可能須使用select, 故須放在重設快取之後
if (!isErr) {
try {
//emit
ee.emit('change', 'insert', data, res)
}
catch (err) {
console.log(err)
}
}
if (isErr) {
return Promise.reject(res)
}
return res
}
async function insert(data) {
if (kpGlock[gkey]) {
await waitFun(() => {
return !kpGlock[gkey]
})
}
kpGlock[gkey] = true
return await insertCore(data)
.finally(() => {
kpGlock[gkey] = false
})
}
/**
* 儲存數據
*
* @memberOf WOrmLowdb
* @param {Object|Array} data 輸入數據物件或陣列
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
* @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
*/
async function saveCore(data, option = {}) {
let isErr = false
//check
if (!iseobj(data) && !isearr(data)) {
return []
}
//cloneDeep, 與外部數據脫勾
data = cloneDeep(data)
//autoInsert
let autoInsert = get(option, 'autoInsert', true)
//res
let res = null
try {
//check
if (!isarr(data)) {
data = [data]
}
//check id
data = map(data, function(v) {
if (!isestr(v.id)) {
v.id = genID()
}
return v
})
//read
await lowdb.read()
//default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
if (!haskey(lowdb.data, key)) {
lowdb.data[key] = []
}
//kp
let kp = {}
each(lowdb.data[key], (v, k) => {
kp[v.id] = { k, v }
})
//pmSeries
let b = false
res = await pmSeries(data, async(v) => {
//rest
let rest = null
//查找資料表內v.id
let r = get(kp, v.id, null)
//existed
let existed = iseobj(r)
//check
if (existed) {
//已存在v.id
if (isEqual(v, r.v)) {
//內容相同不更新
}
else {
//內容不同須更新
//merge
lowdb.data[key][r.k] = merge(lowdb.data[key][r.k], v)
rest = { update: true }
b = true
}
}
else {
//內容不存在
}
//rest
if (iseobj(rest)) {
rest = {
n: 1,
nModified: 1,
ok: 1,
}
}
else {
rest = {
n: 0,
nModified: 0,
ok: 1,
}
}
//rest.n === 0:
// 內容相同不更新, 不須update
// 內容不存在, 若autoInsert則須insert
//autoInsert
if (autoInsert && rest.n === 0 && !existed) {
let k = size(lowdb.data[key])
lowdb.data[key].push(v)
kp[v.id] = {
k,
v: lowdb.data[key][k],
}
b = true
rest = {
n: 1,
nInserted: 1,
ok: 1,
}
}
return rest
})
//write
if (b) {
await lowdb.write()
}
}
catch (err) {
isErr = true
res = err
}
//update, 不能保證變更多少, 一律重設快取
_cache = null
//emit, 於change可能須使用select, 故須放在重設快取之後
if (!isErr) {
try {
//emit
ee.emit('change', 'save', data, res)
}
catch (err) {
console.log(err)
}
}
if (isErr) {
return Promise.reject(res)
}
return res
}
async function save(data, option = {}) {
if (kpGlock[gkey]) {
await waitFun(() => {
return !kpGlock[gkey]
})
}
kpGlock[gkey] = true
return await saveCore(data, option)
.finally(() => {
kpGlock[gkey] = false
})
}
/**
* 刪除數據
*
* @memberOf WOrmLowdb
* @param {Object|Array} data 輸入數據物件或陣列
* @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
*/
async function delCore(data) {
let isErr = false
//check
if (!iseobj(data) && !isearr(data)) {
return []
}
//cloneDeep, 與外部數據脫勾
data = cloneDeep(data)
//res
let res = null
try {
//check
if (!isarr(data)) {
data = [data]
}
//read
await lowdb.read()
//default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
if (!haskey(lowdb.data, key)) {
lowdb.data[key] = []
}
//kp
let kp = {}
each(lowdb.data[key], (v, k) => {
kp[v.id] = { k, v }
})
//pmSeries
let ks = []
let b = false
res = await pmSeries(data, async(v) => {
//rest
let rest = null
//id
let id = get(v, 'id', '')
//check
if (isestr(id)) {
//查找資料表內v.id為_v
let r = get(kp, id, null)
//check
if (iseobj(r)) {
//已存在v.id則須刪除
//push
ks.push(r.k)
b = true
//rest
rest = {
n: 1,
nDeleted: 1,
ok: 1,
}
}
else {
//不存在v.id則不刪除
//rest
rest = {
n: 1,
nDeleted: 0,
ok: 1,
}
}
}
else {
//未給v.id則不刪除
//rest
rest = {
n: 1,
nDeleted: 0,
ok: 0, //未給v.id視為有問題數據, 故ok給0
}
}
return rest
})
//update
if (b) {
lowdb.data[key] = arrPullAt(lowdb.data[key], ks)
}
//write
if (b) {
await lowdb.write()
}
}
catch (err) {
isErr = true
res = err
}
//update, 不能保證刪除多少, 一律重設快取
_cache = null
//emit, 於change可能須使用select, 故須放在重設快取之後
if (!isErr) {
try {
//emit
ee.emit('change', 'del', data, res)
}
catch (err) {
console.log(err)
}
}
if (isErr) {
return Promise.reject(res)
}
return res
}
async function del(data) {
if (kpGlock[gkey]) {
await waitFun(() => {
return !kpGlock[gkey]
})
}
kpGlock[gkey] = true
return await delCore(data)
.finally(() => {
kpGlock[gkey] = false
})
}
/**
* 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
*
* @memberOf WOrmLowdb
* @param {Object} [find={}] 輸入刪除條件物件
* @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
*/
async function delAllCore(find = {}) {
let isErr = false
//res
let res = null
try {
//read
await lowdb.read()
//default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
if (!haskey(lowdb.data, key)) {
lowdb.data[key] = []
}
//filter
let nAll = size(lowdb.data[key])
let nDel = 0
let b = false
if (iseobj(find)) {
//q
let q = new Query(find)
// console.log('q', q)
//find
let _res = q.find(lowdb.data[key]).all()
// console.log('_res', _res)
//nDel
nDel = size(_res)
// console.log('nDel', nDel)
if (nDel === 0) {
//未有find結果等於不刪除
}
else if (nAll === nDel) {
//全在find結果內等於全部刪除
//empty
lowdb.data[key] = []
b = true
}
else {
//部份在find結果內
//_kp
let _kp = {}
each(_res, (v, k) => {
_kp[v.id] = { k, v }
})
//arr
let arr = []
each(lowdb.data[key], (v, k) => {
if (!haskey(_kp, v.id)) {
//未在find結果內代表須保留
arr.push(v)
}
})
//update
lowdb.data[key] = arr
b = true
}
}
else {
//nDel
nDel = nAll
//empty
lowdb.data[key] = []
b = true
}
//write
if (b) {
await lowdb.write()
}
//res
res = {
n: nAll,
nDeleted: nDel,
ok: 1,
}
}
catch (err) {
isErr = true
res = err
}
//update, 不能保證刪除多少, 一律重設快取
_cache = null
//emit, 於change可能須使用select, 故須放在重設快取之後
if (!isErr) {
try {
//emit
ee.emit('change', 'delAll', null, res)
}
catch (err) {
console.log(err)
}
}
if (isErr) {
return Promise.reject(res)
}
return res
}
async function delAll(find = {}) {
if (kpGlock[gkey]) {
await waitFun(() => {
return !kpGlock[gkey]
})
}
kpGlock[gkey] = true
return await delAllCore(find)
.finally(() => {
kpGlock[gkey] = false
})
}
//bind
ee.select = select
ee.insert = insert
ee.save = save
ee.del = del
ee.delAll = delAll
return ee
}
export default WOrmLowdb