import events from 'events'
import Sequelize from 'sequelize'
import cloneDeep from 'lodash/cloneDeep'
import get from 'lodash/get'
import map from 'lodash/map'
import each from 'lodash/each'
import size from 'lodash/size'
import split from 'lodash/split'
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 iser from 'wsemi/src/iser.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import WAutoSequelize from 'w-auto-sequelize/src/WAutoSequelize.mjs'
import importModels from './importModels.mjs'
/**
* 操作資料庫(Microsoft SQL)
*
* 注意: 各model內id欄位不是主鍵(primary key)時需要強制更改成為主鍵,否則sequelize無法匯入
*
* @class
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.url='mssql://username:password@localhost:1433'] 輸入連接資料庫字串,預設'mssql://username:password@localhost:1433'
* @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
* @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
* @param {String} [opt.fdModels='models'] 輸入資料表models(各檔為*.js)所在資料夾字串,預設'models'
* @param {Boolean} [opt.logging=false] 輸入是否輸出實際執行的sql指令,預設false
* @param {String} [opt.pk='id'] 輸入數據主鍵字串,預設'id'
* @param {Boolean} [opt.autoGenPK=true] 輸入若數據pk(id)欄位沒給時則自動給予隨機uuid,預設true
* @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
*/
function WOrmMssql(opt = {}) {
let ss
let u
//default
if (!opt.url) {
opt.url = 'mssql://username:password@localhost:1433'
}
if (!opt.db) {
opt.db = 'worm'
}
if (!opt.cl) {
opt.cl = 'test'
}
if (!opt.fdModels) {
opt.fdModels = 'models'
}
opt.logging = (opt.logging === true)
if (!opt.pk) {
opt.pk = 'id'
}
if (!isbol(opt.autoGenPK)) {
opt.autoGenPK = true
}
//ee
let ee = new events.EventEmitter()
//dialect
let dialect
ss = split(opt.url, '://')
dialect = get(ss, 0, null)
if (!dialect) {
console.log('invalid dialect in opt.url')
return ee
}
u = get(ss, 1, '') //另存給後面使用
//username, password
let username
let password
if (u.indexOf('@') < 0) {
console.log('invalid username or password in opt.url')
return ee
}
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, null)
password = get(ss, 1, null)
if (!username) {
console.log('invalid username in opt.url')
return ee
}
if (!password) {
console.log('invalid password in opt.url')
return ee
}
//host, port
let host
let port
ss = split(u, ':')
if (size(ss) !== 2) {
console.log('invalid host or port in opt.url')
return ee
}
host = get(ss, 0, null)
port = get(ss, 1, null)
if (!host) {
console.log('invalid host in opt.url')
return ee
}
if (!port) {
console.log('invalid port in opt.url')
return ee
}
//sequelize
let sequelize
function initSequelize() {
let err = null
//sequelize
sequelize = new Sequelize(opt.db, username, password, {
dialect,
host,
port,
logging: opt.logging,
define: {
//underscored: false,
//freezeTableName: false,
//syncOnAssociation: true,
//charset: 'utf8',
//dialectOptions: {
// collate: 'utf8_general_ci'
//},
timestamps: false
},
// pool: {
// max: 20,
// min: 0,
// acquire: 30000, //The maximum time, in milliseconds, that pool will try to get connection before throwing error
// idle: 10000 //The maximum time, in milliseconds, that a connection can be idle before being released.
// },
//sync: { force: true }, //強制同步
})
//mds, 若model內id不是pk則需要強制更改成為pk, 否則sequelize無法匯入
let mds = importModels(opt.fdModels, sequelize, opt.cl)
//check
if (iser(mds)) {
err = `can not import model: ${opt.cl}, need to use genModels or create ${opt.cl}.js`
}
return {
mds,
err,
}
}
//Op
let Op = Sequelize.Op
/**
* 查詢數據
*
* @memberOf WOrmMssql
* @param {Object} [find={}] 輸入查詢條件物件
* @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
*/
async function select(find = {}) {
function cvObj(o) {
let oNew = {}
each(o, (v, k) => {
let kNew = k
if (k.indexOf('$') >= 0) {
k = k.replace('$', '')
if (k === 'regex') {
kNew = Op.substring
}
else if (k === 'options') {
kNew = null
}
else if (k === 'nin') {
kNew = Op.notIn
}
else {
kNew = Op[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 = {}
if (isobj(o)) {
oNew = cvObj(o)
}
else {
console.log('select: find is not object')
}
return oNew
}
//useFind
let useFind = cvFind(find)
//initSequelize
let si = initSequelize()
//rs
let rs = null
if (!si.err) {
//md
let md = si.mds[opt.cl]
//findAll
rs = await md.findAll({
where: useFind,
raw: true,
})
}
else {
console.log('select: ', si.err)
}
//close
sequelize.close()
return rs
}
/**
* 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
*
* @memberOf WOrmMssql
* @param {Object|Array} data 輸入數據物件或陣列
* @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
*/
async function insert(data) {
//cloneDeep
data = cloneDeep(data)
//pm
let pm = genPm()
//initSequelize
let si = initSequelize()
if (!si.err) {
//md
let md = si.mds[opt.cl]
//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
})
}
//bulkCreate
await md.bulkCreate(data)
.then((res) => {
//console.log('bulkCreate then',res)
res = { n: size(data), ok: 1 }
pm.resolve(res)
ee.emit('change', 'insert', data, res)
})
.catch(({ original }) => {
//console.log('bulkCreate catch',original)
pm.reject({ n: 0, ok: 0 })
})
}
else {
pm.reject(si.err)
}
//close
sequelize.close()
return pm
}
/**
* 儲存數據
*
* @memberOf WOrmMssql
* @param {Object|Array} data 輸入數據物件或陣列
* @param {Object} [option={}] 輸入設定物件,預設為{}
* @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
* @param {boolean} [option.atomic=false] 輸入是否於儲存時採用上鎖,避免同時操作互改問題,預設為false
* @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
*/
async function save(data, option = {}) {
//cloneDeep
data = cloneDeep(data)
//autoInsert, atomic
let autoInsert = get(option, 'autoInsert', true)
let atomic = get(option, 'atomic', false)
//pm
let pm = genPm()
//initSequelize
let si = initSequelize()
if (!si.err) {
//md
let md = si.mds[opt.cl]
//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
})
}
//tr
let t = null
let tr = {}
if (atomic) {
t = await sequelize.transaction()
tr = {
transaction: t
}
}
//pmSeries
await pmSeries(data, async function(v) {
let pmm = genPm()
//err
let err = null
//r
let r
if (v[opt.pk]) {
//有id
//findOne
r = await md.findOne({
where: { [opt.pk]: v[opt.pk] },
raw: true,
})
.catch((error) => {
err = error
})
}
else {
//沒有id
err = `${opt.pk} is invalid`
}
if (r) {
//有找到資料
let rr = await md.update(v, {
where: { [opt.pk]: v[opt.pk] },
...tr,
})
.catch((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: 0, nInserted: 0, ok: 1 })
}
}
else {
//沒有找到資料
//autoInsert
if (autoInsert) {
//create
let rr = await md.create(v, tr)
.catch((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: 0, nInserted: 0, ok: 1 })
}
}
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)
if (t) {
//console.log('transaction commit')
return t.commit()
}
})
.catch((res) => {
pm.reject(res)
if (t) {
//console.log('transaction rollback')
return t.rollback()
}
})
}
else {
pm.reject(si.err)
}
//close
sequelize.close()
return pm
}
/**
* 刪除數據
*
* @memberOf WOrmMssql
* @param {Object|Array} data 輸入數據物件或陣列
* @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
*/
async function del(data) {
//cloneDeep
data = cloneDeep(data)
//pm
let pm = genPm()
//initSequelize
let si = initSequelize()
if (!si.err) {
//md
let md = si.mds[opt.cl]
//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
r = await md.findOne({
where: { [opt.pk]: v[opt.pk] },
raw: true,
})
.catch((error) => {
err = error
})
}
else {
//沒有id
err = `${opt.pk} is invalid`
}
if (r) {
//有找到資料
//destroy
let rr = await md.destroy({
where: { [opt.pk]: v[opt.pk] },
})
.catch((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: 0, nDeleted: 0, ok: 1 })
}
}
else {
//console.log('findOne 沒有找到資料', err)
pmm.resolve({ n: 1, nDeleted: 1, 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)
}
//close
sequelize.close()
return pm
}
/**
* 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
*
* @memberOf WOrmMssql
* @param {Object} [find={}] 輸入刪除條件物件
* @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
*/
async function delAll(find = {}) {
//pm
let pm = genPm()
//initSequelize
let si = initSequelize()
if (!si.err) {
//md
let md = si.mds[opt.cl]
//destroy
await md.destroy({
where: find,
})
.then((res) => {
res = { n: res, ok: 1 }
pm.resolve(res)
ee.emit('change', 'delAll', null, res)
})
.catch((res) => {
pm.reject({ n: 0, ok: 1 })
})
}
else {
pm.reject(si.err)
}
//close
sequelize.close()
return pm
}
/**
* 由指定資料庫生成各表的models資料
*
* include from: [w-auto-sequelize](https://github.com/yuda-lyu/w-auto-sequelize)
*
* @memberOf WOrmMssql
* @param {Object} [option={}] 輸入設定物件,預設{}
* @param {String} [option.database=null] 輸入資料庫名稱字串,預設null
* @param {String} [option.username=null] 輸入使用者名稱字串,預設null
* @param {String} [option.password=null] 輸入密碼字串,預設null
* @param {String} [option.dialect=null] 輸入資料庫種類字串,預設null,可選'mysql', 'mariadb', 'sqlite', 'postgres', 'mssql'
* @param {String} [option.directory='./models'] 輸入models儲存的資料夾名稱字串,預設'./models'
* @param {String} [option.host='localhost'] 輸入連線主機host位址字串,預設'localhost'
* @param {Integer} [option.port=null] 輸入連線主機port整數,預設null
* @returns {Promise} 回傳Promise,resolve回傳產生的models資料,reject回傳錯誤訊息
*/
function genModels(option = {}) {
//default
let def = {
//database: 'database',
username: 'username',
password: 'password',
dialect: 'mssql',
//directory: './models',
host: 'localhost',
port: 1433,
}
//merge
option = {
...def,
...option,
}
//database
if (!option.database) {
option.database = opt.db
}
//directory
if (!option.directory) {
option.directory = opt.fdModels
}
//WAutoSequelize
return WAutoSequelize(option)
}
//bind
ee.genModels = genModels
ee.select = select
ee.insert = insert
ee.save = save
ee.del = del
ee.delAll = delAll
return ee
}
export default WOrmMssql