import fs from 'fs'
import path from 'path'
import JSON5 from 'json5'
import Sequelize from 'sequelize'
import each from 'lodash-es/each.js'
import get from 'lodash-es/get.js'
import trim from 'lodash-es/trim.js'
import join from 'lodash-es/join.js'
import iseobj from 'wsemi/src/iseobj.mjs'
import genPm from 'wsemi/src/genPm.mjs'
import requireModel from 'w-orm-reladb/src/requireModel.js'
//匯入資料表設定檔(models)
//
//本模組取代w-orm-reladb之importModels,差異僅在於modifyModel之寫檔方式:
//原版於[每一次]匯入時皆無條件以fs.writeFileSync重寫使用者之model原始檔,
//而本套件每次操作(select/insert/save/del…)皆會匯入一次,故等同每次操作都重寫該檔。
//兩個行程併發操作同一資料表時,一方正截斷檔案而另一方讀取,即讀到空內容,
//匯入失敗且該檔[永久損毀](已實機重現: models/users.js被截為0位元組,其後所有行程之操作全數失敗)。
//
//本版之修正為三:
// 1. 內容未變更者完全不寫檔——model已具主鍵設定時(常態)即不再有任何寫入
// 2. 已具主鍵設定者一律原樣返回而不重新產生內容,令行尾字元(CRLF)與格式皆不被改動;
// 原版係以'\n'重組全檔,Windows之CRLF檔因此每次皆被判定為[有變更]而重寫
// 3. 確需變更者改以[寫暫存檔後rename]之原子寫入,令並行讀取者不會讀到中途狀態
/**
* 強制js格式之model將id設為主鍵
*
* 註: 若id沒有被設定為pk, 則需要強制設為pk, 否則sequelize無法匯入
*
* @ignore
* @param {String} h 輸入model檔內容字串
* @returns {String} 回傳處理後之內容字串
*/
function forJS(h) {
//eol, 保留原檔之行尾字元, 否則Windows之CRLF檔會被正規化為LF而每次皆判定為[有變更]
let eol = h.indexOf('\r\n') >= 0 ? '\r\n' : '\n'
//s
let s = h.split(/\r?\n/)
//find ind
let indIDs = null
let indIDe = null
let indHasPK = false
each(s, (v, k) => {
v = trim(v)
if (v === `'id': {`) {
indIDs = k
}
if (indIDs !== null && indIDe === null && v.indexOf('primaryKey: true') >= 0) {
indHasPK = true
}
if (indIDs !== null && v === '}') {
indIDe = k
}
})
//check, 無id欄位或已設為主鍵者原樣返回, 令常態下完全不觸碰使用者之原始檔
if (indIDs === null || indHasPK) {
return h
}
//add primaryKey
s[indIDs] += 'primaryKey: true,' //用json5轉不用考慮是否最末不補逗號
return join(s, eol)
}
/**
* 強制json格式之model將id設為主鍵
*
* @ignore
* @param {String} h 輸入model檔內容字串
* @returns {String} 回傳處理後之內容字串
*/
function forJSON(h) {
//s
let s = JSON5.parse(h)
//check
if (!iseobj(get(s, 'fields'))) {
throw new Error('invalid fields')
}
//自動針對id設定為主鍵
let bMod = false
each(s.fields, (v, k) => {
if (k === 'id' && v.primaryKey !== true) {
v.primaryKey = true
bMod = true
}
})
//check, 已設為主鍵者原樣返回而不重新序列化, 令常態下完全不觸碰使用者之原始檔
if (!bMod) {
return h
}
return JSON5.stringify(s, null, 4)
}
/**
* 視需要修正model檔,令id為主鍵
*
* 註: 內容未變更者不寫檔,確需變更者以原子寫入完成,見本檔頭之說明
*
* @ignore
* @param {String} fn 輸入model檔位置字串
* @param {Object} [opt={}] 輸入設定物件
* @param {String} [opt.type='js'] 輸入資料表設定檔類型字串
* @returns {undefined} 無回傳值
*/
function modifyModel(fn, opt = {}) {
//type
let type = get(opt, 'type')
if (type !== 'js' && type !== 'json') {
type = 'js'
}
//h
let h = fs.readFileSync(fn, 'utf8')
//h2
let h2 = h
if (type === 'js') {
h2 = forJS(h)
}
else if (type === 'json') {
h2 = forJSON(h)
}
//check, 內容未變更即不寫檔, 令常態下完全不觸碰使用者之原始檔
if (h2 === h) {
return
}
//write, 以寫暫存檔後rename之原子寫入完成, 令並行讀取者不會讀到中途狀態
let fnTemp = `${fn}.${process.pid}.tmp`
try {
fs.writeFileSync(fnTemp, h2, 'utf8')
fs.renameSync(fnTemp, fn)
}
catch (err) {
try {
fs.unlinkSync(fnTemp)
}
catch (errDel) {}
throw err
}
}
/**
* 讀取js格式之model
*
* @ignore
*/
function readJsModel(fn, sequelize) {
return requireModel(fn, sequelize, Sequelize.DataTypes)
}
/**
* 讀取json格式之model
*
* @ignore
*/
function readJsonModel(fn, sequelize) {
//s
let j = fs.readFileSync(fn, 'utf8')
let s = JSON5.parse(j)
//抽換DataTypes, 例如將'DataTypes.TEXT'改為DataTypes.TEXT
each(s.fields, (v) => {
v.type = get(Sequelize, v.type, null)
})
//model, 使用define產生model
return sequelize.define(s.table, s.fields, s.options)
}
/**
* 匯入資料表設定檔
*
* @param {String} fdModels 輸入models所在資料夾字串
* @param {Object} sequelize 輸入sequelize實例
* @param {String} name 輸入資料表名稱字串
* @param {Object} [opt={}] 輸入設定物件
* @param {String} [opt.type='js'] 輸入資料表設定檔類型字串,可有'js'、'json'
* @param {Boolean} [opt.sync=false] 輸入是否將models資料同步至資料庫
* @returns {Promise} 回傳Promise,resolve回傳models物件,reject回傳錯誤訊息
*/
async function importModels(fdModels, sequelize, name, opt = {}) {
//type
let type = get(opt, 'type')
if (type !== 'js' && type !== 'json') {
type = 'js'
}
//sync
let sync = get(opt, 'sync') === true
//pm
let pm = genPm()
//fn
let fn = path.resolve(fdModels) + path.sep + `${name}.${type}`
//modifyModel
try {
modifyModel(fn, { type })
}
catch (err) {
pm.reject(err)
return pm
}
//import
let model
try {
if (type === 'js') {
model = readJsModel(fn, sequelize)
}
else if (type === 'json') {
model = readJsonModel(fn, sequelize)
}
}
catch (err) {
pm.reject(err)
return pm
}
//sync
try {
if (sync) {
await model.sync()
}
}
catch (err) {
pm.reject(err)
return pm
}
//models
let models = {}
models[model.name] = model
//resolve
pm.resolve(models)
return pm
}
export default importModels