import path from 'path'
import events from 'events'
import Sequelize from 'sequelize'
import Adodb from 'node-adodb'
import cloneDeep from 'lodash-es/cloneDeep.js'
import get from 'lodash-es/get.js'
import map from 'lodash-es/map.js'
import each from 'lodash-es/each.js'
import size from 'lodash-es/size.js'
import split from 'lodash-es/split.js'
import genPm from 'wsemi/src/genPm.mjs'
import genID from 'wsemi/src/genID.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import pmQueue from 'wsemi/src/pmQueue.mjs'
import _genModelsByTabs from 'w-orm-reladb/src/genModelsByTabs.mjs'
import { getSelect, getInsert, getUpdate, getDelete } from './getCmd.mjs'
import { initSequelize, queryMemory } from './queryMemory.mjs'
//放置於全域宣告, 若使用useStable=true才可於當創建多實例時仍能全域控管
let pmq = pmQueue(1)
/**
* 操作關聯式資料庫
*
* @class
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.url='mdb://username:password'] 輸入連接資料庫字串,資料庫僅可選'mdb',預設'mdb://username:password'
* @param {String} [opt.storage='./worm.db'] 輸入mdb資料庫檔案位置字串,預設'./worm.db'
* @param {Boolean} [opt.useEncryption=false] 輸入是否使用加密保護,型別為布林值,預設false
* @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,mdb並不需要給只是配合w-orm系列統一設定,預設'worm'
* @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
* @param {String} [opt.fdModels='./models'] 輸入資料表設定檔所在資料夾字串,預設'./models'
* @param {String} [opt.dbType='Access2007'] 輸入使用資料庫類型字串,可選'Access2000-2003'、'Access2007',預設'Access2007'
* @param {String} [opt.modelType='js'] 輸入資料表設定檔類型字串,可有'js'、'json',預設'js'
* @param {Boolean} [opt.logging=false] 輸入是否輸出實際執行的sql指令布林值,預設false
* @param {String} [opt.pk='id'] 輸入數據主鍵字串,預設'id'
* @param {Boolean} [opt.autoGenPK=true] 輸入若數據pk(id)欄位沒給時則自動給予隨機uuid,型別為布林值,預設true
* @param {Boolean} [opt.useStable=true] 輸入是否使用穩定模式,使用佇列管理同時只能進行一種操作故會犧牲效能,mdb需開啟穩定模式才不會有非預期錯誤,型別為布林值,預設true
* @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
*/
function WOrmMdb(opt = {}) {
let ss
let u
let sequelize = null
let adodb = null
//default
if (!isestr(opt.url)) {
opt.url = 'mdb://username:password'
}
if (!isestr(opt.db)) {
opt.db = 'worm'
}
if (!isestr(opt.cl)) {
opt.cl = 'test'
}
if (!isestr(opt.fdModels)) {
opt.fdModels = './models'
}
if (!isestr(opt.modelType)) {
opt.modelType = 'js'
}
opt.logging = (opt.logging === true)
if (!isestr(opt.pk)) {
opt.pk = 'id'
}
if (!isbol(opt.autoGenPK)) {
opt.autoGenPK = true
}
if (!isbol(opt.useStable)) {
opt.useStable = true
}
if (!isestr(opt.storage)) {
opt.storage = './worm.mdb'
}
opt.storage = path.resolve(opt.storage)
if (opt.dbType !== 'Access2000-2003' && opt.dbType !== 'Access2007' && opt.dbType !== 'Access2016') {
opt.dbType = 'Access2007'
}
if (!isbol(opt.useEncryption)) {
opt.useEncryption = false
}
//Op
let Op = Sequelize.Op
// let operatorsAliases = {
// $eq: Op.eq,
// $ne: Op.ne,
// $gte: Op.gte,
// $gt: Op.gt,
// $lte: Op.lte,
// $lt: Op.lt,
// $not: Op.not,
// $in: Op.in,
// $notIn: Op.notIn,
// $is: Op.is,
// $like: Op.like,
// $notLike: Op.notLike,
// $iLike: Op.iLike,
// $notILike: Op.notILike,
// $regexp: Op.regexp,
// $notRegexp: Op.notRegexp,
// $iRegexp: Op.iRegexp,
// $notIRegexp: Op.notIRegexp,
// $between: Op.between,
// $notBetween: Op.notBetween,
// $overlap: Op.overlap,
// $contains: Op.contains,
// $contained: Op.contained,
// $adjacent: Op.adjacent,
// $strictLeft: Op.strictLeft,
// $strictRight: Op.strictRight,
// $noExtendRight: Op.noExtendRight,
// $noExtendLeft: Op.noExtendLeft,
// $substring: Op.substring,
// $and: Op.and,
// $or: Op.or,
// $any: Op.any,
// $all: Op.all,
// $values: Op.values,
// $col: Op.col
// }
//ee
let ee = new events.EventEmitter()
//dialect
let dialect
ss = split(opt.url, '://')
dialect = get(ss, 0, null)
if (!dialect) {
console.log('no dialect in opt.url')
return ee
}
if (dialect !== 'mdb') {
console.log(`dialect[${dialect}] is not mdb`)
return ee
}
u = get(ss, 1, '') //另存給後面使用
//username, password
let username
let password
ss = split(u, '@')
u = get(ss, 1, '') //另存給後面使用
ss = get(ss, 0, '')
ss = split(ss, ':')
if (size(ss) !== 2) {
console.log('invalid username or password in opt.url')
return ee
}
username = get(ss, 0, '')
password = get(ss, 1, '')
if (opt.useEncryption) {
if (username === '') {
console.log('invalid username in opt.url')
return ee
}
if (password === '') {
console.log('invalid password in opt.url')
return ee
}
}
/**
* 初始化adodb
*
* @memberOf WOrmMdb
* @returns {Promise} 回傳Promise,resolve代表關閉成功,reject回傳錯誤訊息
*/
async function initAdodb() {
let err = null
//provider
let kpProvider = {
'Access2000-2003': 'Microsoft.Jet.OLEDB.4.0',
'Access2007': 'Microsoft.ACE.OLEDB.12.0',
'Access2016': 'Microsoft.ACE.OLEDB.16.0',
}
let provider = kpProvider[opt.dbType]
// console.log('provider', provider)
//databasePassword
let databasePassword = ''
if (opt.useEncryption) {
let up = `${username}:${password}`
databasePassword = ` Jet OLEDB:Database Password=${up};`
}
//strConn
let strConn = `Provider=${provider};Data Source=${opt.storage};Persist Security Info=False;${databasePassword}`
// console.log('strConn', strConn)
//adodb, open for x64 記憶體才能支撐讀大檔
adodb = Adodb.open(strConn, true)
// console.log('adodb', adodb)
//initSequelize
let r = await initSequelize(opt)
//save
sequelize = r.sequelize
let mds = r.mds
return {
mds,
err,
close: () => {
return closeSequelize('external')
},
}
}
/**
* 關閉sequelize
*
* @memberOf WOrmReladb
* @returns {Promise} 回傳Promise,resolve代表關閉成功,reject回傳錯誤訊息
*/
async function closeSequelize(from) {
if (sequelize !== null) {
await sequelize.close()
//console.log(from, 'sequelize.close()')
}
sequelize = null
//console.log(from, 'sequelize = null')
}
// /**
// * 產生交易transaction狀態物件,當使用transaction時資料庫會上鎖,只能供調用的連線操作處理
// *
// * @memberOf WOrmMdb
// * @returns {Promise} 回傳Promise,resolve回傳交易transaction物件,reject回傳錯誤訊息
// */
// async function genTransaction() {
// let t
// // if (sequelize !== null) {
// // t = await sequelize.transaction() //使用Unmanaged transaction (then-callback)
// // // t.afterCommit(() => {
// // // console.log('afterCommit')
// // // })
// // }
// // else {
// // return Promise.reject('invalid sequelize')
// // }
// return t
// }
function getUseFind(find) {
function getKNew(k) {
let kNew = null
if (k === 'regex') {
kNew = Op.substring
}
else if (k === 'options') {
kNew = null
}
else if (k === 'nin') {
kNew = Op.notIn
}
else {
kNew = Op[k]
}
// if (k === 'regex') {
// kNew = 'substring' //Op.substring
// }
// else if (k === 'options') {
// kNew = null
// }
// else if (k === 'nin') {
// kNew = 'notIn' //Op.notIn
// }
// else {
// kNew = k //Op[k]
// }
// if (kNew !== null) {
// kNew = '$' + kNew
// }
return kNew
}
function cvObj(o) {
let oNew = {}
each(o, (v, k) => {
let kNew = k
if (k.indexOf('$') >= 0) {
k = k.replace('$', '')
kNew = getKNew(k) //只針對開頭為$的指令轉譯
}
let vNew = v
if (isarr(v)) {
vNew = cvArray(v)
}
else if (isobj(v)) {
vNew = cvObj(v)
}
if (kNew !== null) {
oNew[kNew] = vNew
}
})
return oNew
}
function cvArray(o) {
let oNew = []
each(o, (v) => {
let vNew = v
if (isarr(v)) {
vNew = cvArray(v)
}
else if (isobj(v)) {
vNew = cvObj(v)
}
oNew.push(vNew)
})
return oNew
}
function cvFind(o) {
let oNew = cvObj(o)
return oNew
}
//find
if (!isobj(find)) {
find = {}
}
find = cloneDeep(find)
//useFind
let useFind = cvFind(find)
return useFind
}
async function findAll(si, find) {
async function selectAll(si, find) {
//useFind
let useFind = getUseFind(find)
//md
let md = si.mds[opt.cl]
//getSelect
let cmd = await getSelect(opt.cl, md, useFind)
//query
let rs = await adodb.query(cmd)
return rs
}
//data, 先查詢出access mdb內全部資料
let data = await selectAll(si, {})
//useFind
let useFind = getUseFind(find)
//query
let rs = await queryMemory(opt, data, useFind)
return rs
}
async function findOne(si, find) {
let rs = await findAll(si, find)
let r = get(rs, 0, null)
return r
}
async function insertOne(si, data) {
//md
let md = si.mds[opt.cl]
//getInsert
let cmd = await getInsert(opt.cl, md, data)
//execute
await adodb.execute(cmd)
return 'ok'
}
async function insertAll(si, data) {
//node-adodb 5.0.3不支援transaction(舊版應該有支援), 查ADODB class內沒有transaction, 應該是還在修復中無法用, 故只能1次1組
//pm
let pm = genPm()
//pmSeries
let i = 0
await pmSeries(data, async(v) => {
//insertOne
await insertOne(si, v)
//count
i++
})
.then(() => {
pm.resolve('ok')
})
.catch((err) => {
pm.reject({
err,
i,
})
})
return pm
}
async function updateOne(si, data, find) {
//useFind
let useFind = getUseFind(find)
//md
let md = si.mds[opt.cl]
//getUpdate
let cmd = await getUpdate(opt.cl, md, data, useFind)
//execute
await adodb.execute(cmd)
return 'ok'
}
async function destroyAll(si, find) {
//useFind
let useFind = getUseFind(find)
//md
let md = si.mds[opt.cl]
//getUpdate
let cmd = await getDelete(opt.cl, md, useFind)
//execute
await adodb.execute(cmd)
return 'ok'
}
/**
* 查詢數據
*
* @memberOf WOrmMdb
* @param {Object} [find={}] 輸入查詢條件物件
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
* @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
* @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
*/
async function select(find = {}, option = {}) {
//instance
let instance = get(option, 'instance', null)
// //transaction
// let transaction = get(option, 'transaction', null)
//si
let si = instance
if (instance === null) {
si = await initAdodb()
}
//rs
let rs = null
if (!si.err) {
//findAll
rs = await findAll(si, find)
}
else {
ee.emit('error', si.err)
}
//closeSequelize
if (instance === null) { //內部自動初始化得close
await closeSequelize('select')
}
return rs
}
/**
* 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
*
* @memberOf WOrmMdb
* @param {Object|Array} data 輸入數據物件或陣列
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
* @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
* @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
*/
async function insert(data, option = {}) {
//instance
let instance = get(option, 'instance', null)
// //transaction
// let transaction = get(option, 'transaction', null)
//cloneDeep
data = cloneDeep(data)
//pm
let pm = genPm()
//si
let si = instance
if (instance === null) {
si = await initAdodb()
}
if (!si.err) {
//check
if (!isarr(data)) {
data = [data]
}
//n
let n = size(data)
//check
if (opt.autoGenPK) {
data = map(data, function(v) {
if (!v[opt.pk]) {
v[opt.pk] = genID()
}
return v
})
}
//pmSeries
await insertAll(si, data)
.then(() => {
let res = { n, nInserted: n, ok: 1 }
pm.resolve(res)
ee.emit('change', 'insert', data, res)
})
.catch(({ err, i }) => {
ee.emit('error', err)
pm.reject({ n: i, ok: 0 })
})
}
else {
pm.reject(si.err)
}
//closeSequelize
if (instance === null) { //內部自動初始化得close
await closeSequelize('select')
}
return pm
}
/**
* 儲存數據
*
* @memberOf WOrmMdb
* @param {Object|Array} data 輸入數據物件或陣列
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
* @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
* @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
* @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
*/
async function save(data, option = {}) {
//cloneDeep
data = cloneDeep(data)
//instance
let instance = get(option, 'instance', null)
// //transaction
// let transaction = get(option, 'transaction', null)
//autoInsert
let autoInsert = get(option, 'autoInsert', true)
//pm
let pm = genPm()
//si
let si = instance
if (instance === null) {
si = await initAdodb()
}
if (!si.err) {
//check
if (!isarr(data)) {
data = [data]
}
//check
if (opt.autoGenPK) {
data = map(data, function(v) {
if (!v[opt.pk]) {
v[opt.pk] = genID()
}
return v
})
}
//pmSeries
await pmSeries(data, async(v) => {
let pmm = genPm()
//err
let err = null
//r
let r
if (v[opt.pk]) {
//有id
//findOne
let find = { [opt.pk]: v[opt.pk] }
r = await findOne(si, find)
.catch((error) => {
ee.emit('error', error)
err = error
})
}
else {
//沒有id
err = `${opt.pk} is invalid`
}
if (r) {
//有找到資料
//updateOne
let find = { [opt.pk]: v[opt.pk] }
let rr = await updateOne(si, v, find)
.catch((error) => {
ee.emit('error', error)
err = error
})
if (rr) {
//console.log('update 有更新資料', rr)
pmm.resolve({ n: 1, nModified: 1, ok: 1 })
}
else {
//console.log('update 沒有更新資料', err)
pmm.resolve({ n: 1, nModified: 0, ok: 0 })
}
}
else {
//沒有找到資料
//autoInsert
if (autoInsert) {
//insertOne
let rr = await insertOne(si, v)
.catch((error) => {
ee.emit('error', error)
err = error
})
if (rr) {
//console.log('create 有插入資料', rr)
pmm.resolve({ n: 1, nInserted: 1, ok: 1 })
}
else {
//console.log('create 沒有插入資料', err)
pmm.resolve({ n: 1, nInserted: 0, ok: 0 })
}
}
else {
//console.log('findOne 沒有找到資料也不自動插入', err)
pmm.resolve({ n: 0, nModified: 0, ok: 1 })
}
}
pmm._err = err //避免eslint錯誤訊息
return pmm
})
.then((res) => {
pm.resolve(res)
ee.emit('change', 'save', data, res)
})
.catch((error) => {
pm.reject(error)
})
}
else {
pm.reject(si.err)
}
//closeSequelize
if (instance === null) { //內部自動初始化得close
await closeSequelize('select')
}
return pm
}
/**
* 刪除數據
*
* @memberOf WOrmMdb
* @param {Object|Array} data 輸入數據物件或陣列
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
* @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
* @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
*/
async function del(data, option = {}) {
//cloneDeep
data = cloneDeep(data)
//instance
let instance = get(option, 'instance', null)
// //transaction
// let transaction = get(option, 'transaction', null)
//pm
let pm = genPm()
//si
let si = instance
if (instance === null) {
si = await initAdodb()
}
if (!si.err) {
//check
if (!isarr(data)) {
data = [data]
}
//pmSeries
await pmSeries(data, async function(v) {
let pmm = genPm()
//err
let err = null
//r
let r
if (v[opt.pk]) {
//有id
//findOne
let find = { [opt.pk]: v[opt.pk] }
r = await findOne(si, find)
.catch((error) => {
ee.emit('error', error)
err = error
})
}
else {
//沒有id
err = `${opt.pk} is invalid`
}
if (r) {
//有找到資料
//destroyAll
let find = { [opt.pk]: v[opt.pk] }
let rr = await destroyAll(si, find)
.catch((error) => {
ee.emit('error', error)
err = error
})
if (rr) {
//console.log('destroy 有刪除資料', rr)
pmm.resolve({ n: 1, nDeleted: 1, ok: 1 })
}
else {
//console.log('destroy 沒有刪除資料', err)
pmm.resolve({ n: 1, nDeleted: 0, ok: 0 })
}
}
else {
//console.log('findOne 沒有找到資料', err)
pmm.resolve({ n: 0, nDeleted: 0, ok: 1 })
}
pmm._err = err //避免eslint錯誤訊息
return pmm
})
.then((res) => {
pm.resolve(res)
ee.emit('change', 'del', data, res)
})
.catch((res) => {
pm.reject(res)
})
}
else {
pm.reject(si.err)
}
return pm
}
/**
* 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
*
* @memberOf WOrmMdb
* @param {Object} [find={}] 輸入刪除條件物件
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
* @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
* @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
*/
async function delAll(find = {}, option = {}) {
//find
if (!isobj(find)) {
find = {}
}
find = cloneDeep(find)
//instance
let instance = get(option, 'instance', null)
// //transaction
// let transaction = get(option, 'transaction', null)
//pm
let pm = genPm()
//si
let si = instance
if (instance === null) {
si = await initAdodb()
}
if (!si.err) {
//findAll, 因access mdb沒辦法回傳刪除的列數量, 只好多耗功先查詢一次再刪
let rs = await findAll(si, find)
//n
let n = size(rs)
//destroyAll
await destroyAll(si, find)
.then((res) => {
res = { n, nDeleted: n, ok: 1 }
pm.resolve(res)
ee.emit('change', 'delAll', null, res)
})
.catch((error) => {
ee.emit('error', error)
pm.reject({ n: 0, ok: 0 })
})
}
else {
pm.reject(si.err)
}
//closeSequelize
if (instance === null) { //內部自動初始化得close
await closeSequelize('select')
}
return pm
}
/**
* 創建mdb資料庫檔案
*
* @memberOf WOrmMdb
* @returns {Promise} 回傳Promise,resolve回傳創建結果,reject回傳錯誤訊息
*/
async function createStorage() {
console.log('還不能createStorage產生mdb檔')
//pm
let pm = genPm()
// //initAdodb
// let si = await initAdodb()
// //closeSequelize
// await closeSequelize('createStorage')
// //check
// if (si.err) {
// pm.reject(si.err)
// }
// else {
// pm.resolve('created')
// }
return pm
}
// /**
// * 由指定資料庫生成各資料表的models資料
// *
// * 目前僅能產生js檔格式,且mdb欄位為nvarchar(MAX)無法自動轉成TEXT格式
// *
// * include from: [w-auto-sequelize](https://github.com/yuda-lyu/w-auto-sequelize)
// *
// * @memberOf WOrmMdb
// * @param {Object} [option={}] 輸入設定物件,預設{}
// * @param {String} [option.storage='./worm.db'] 輸入mdb資料庫檔案位置字串,預設'./worm.db'
// * @param {String} [option.db='worm'] 輸入資料庫名稱字串,預設'worm'
// * @param {String} [option.username='username'] 輸入使用者名稱字串,預設'username'
// * @param {String} [option.password='password'] 輸入密碼字串,預設'password'
// * @param {String} [option.fdModels='./models'] 輸入models儲存的資料夾名稱字串,預設'./models'
// * @param {String} [option.host='localhost'] 輸入連線主機host位址字串,預設'localhost'
// * @param {Integer} [option.port=1433] 輸入連線主機port整數,預設1433
// * @returns {Promise} 回傳Promise,resolve回傳產生的models資料,reject回傳錯誤訊息
// */
// function genModelsByDB(option = {}) {
// //default
// let def = {
// username: 'username',
// password: 'password',
// storage: './worm.mdb',
// }
// //merge
// option = {
// ...def,
// ...option,
// }
// //database
// if (!option.db) {
// option.db = 'worm'
// }
// option.database = opt.db
// //directory
// if (!option.fdModels) {
// option.fdModels = './models'
// }
// option.directory = opt.fdModels
// //storage
// option.storage = path.resolve(option.storage)
// //WAutoSequelize
// return WAutoSequelize(option)
// }
/**
* 由資料表物件生成各資料表的models資料
*
* @memberOf WOrmMdb
* @param {String} [fd='./models'] 輸入models儲存的資料夾名稱字串,預設'./models'
* @param {Object} [tabs={}] 輸入各資料表物件,預設{}
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.type='js'] 輸入資料表類型字串,預設'js'
*/
function genModelsByTabs(...input) {
_genModelsByTabs(...input)
}
//bind
ee.createStorage = createStorage
// ee.genModelsByDB = genModelsByDB
ee.genModelsByTabs = genModelsByTabs
ee.init = initAdodb
// ee.genTransaction = genTransaction
if (opt.useStable) {
//用佇列(同時最大執行數1且先進先執行)處理高併發之情形
//若沒管控:
//access mdb有機會出錯[Error: ConnectionManager.getConnection was called after the connection manager was closed!]
ee.select = function() {
return pmq(select, ...arguments)
}
ee.insert = function() {
return pmq(insert, ...arguments)
}
ee.save = function() {
return pmq(save, ...arguments)
}
ee.del = function() {
return pmq(del, ...arguments)
}
ee.delAll = function() {
return pmq(delAll, ...arguments)
}
}
else {
ee.select = select
ee.insert = insert
ee.save = save
ee.del = del
ee.delAll = delAll
}
return ee
}
export default WOrmMdb