const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const FileBasedStoreLoader = require('@unitybase/base').FileBasedStoreLoader
const csShared = require('@unitybase/cs-shared')
const UBDomain = csShared.UBDomain
const LocalDataStore = csShared.LocalDataStore
const App = require('@unitybase/ub').App
const UB = require('@unitybase/ub')
const blobStores = App.blobStores
const mStorage = UB.mixins.mStorage
const DFM_CONTENT_TYPE = 'text/javascript; charset=UTF-8'
const REL_PATH_TAIL = 'forms'
const DEF_FILE_TAIL = '-fm.def'
const JS_FILE_TAIL = '-fm.js'
/* global ubm_form ncrc32 */
// eslint-disable-next-line camelcase
let me = ubm_form
me.entity.addMethod('select')
me.entity.addMethod('update')
me.entity.addMethod('insert')
me.entity.addMethod('addnew')
// here we store loaded forms
let resultDataCache = null
let modelLoadDate
/**
* Check integrity of file content. Passed as a callback to FileBasedStore.onBeforeRowAdd
* @private
* @param {FileBasedStoreLoader} loader
* @param {String} fullFilePath
* @param {String} content
* @param {Object} row
* @return {boolean}
*/
function postProcessing (loader, fullFilePath, content, row) {
let jsFilePath
// check entity exist in domain
let val = row.entity
if (!App.domainInfo.has(val)) {
console.error(`ubm_form: Invalid //@entity attribute "${val}". File ${fullFilePath} ignored`)
return false
}
// check fileName = entity code + "-fm.def"
let fileName = path.basename(fullFilePath)
if (row.code) { console.warn(`Please, remove a row //@code "${row.code}" from a file ${fileName}. In UB4 form code = file name without -fm.def`) }
row.code = fileName.substring(0, fileName.length - 7)
if (row.ID) console.warn(`Please, remove a row "//@ID ${row.ID}" from a file ${fileName}. In UB4 form ID is generated automatically as crc32(code)`)
row.ID = ncrc32(0, row.code)
// form can be stored in other model than entity
// we fill relPath in form "modelName"|"path inside model public folder" as expected by mdb virtual store
let relPath = (row.model || loader.processingRootFolder.model.name) + '|' + REL_PATH_TAIL
// fill formDef attribute value
row.formDef = JSON.stringify({
fName: fileName,
origName: fileName,
ct: DFM_CONTENT_TYPE,
size: content.length,
md5: 'fb6a51668017be0950bd18c2fb0474a0',
relPath: relPath
})
if (!row.model) {
row.model = loader.processingRootFolder.model.name
}
// in case form js exist - fill formCode
fileName = fileName.substring(0, fileName.length - DEF_FILE_TAIL.length) + JS_FILE_TAIL
jsFilePath = path.join(path.dirname(fullFilePath), fileName)
if (fs.existsSync(jsFilePath)) { // file exists
let jsFileStat = fs.statSync(jsFilePath)
row.formCode = JSON.stringify({
fName: fileName,
origName: fileName,
ct: DFM_CONTENT_TYPE, // JSON_CONTENT_TYPE,
size: jsFileStat.size,
md5: 'fb6a51668017be0950bd18c2fb0474a0',
relPath: relPath
})
// check js file modification and if later when def file - replace mi_modifyDate
if (loader.haveModifyDate && row.mi_modifyDate < jsFileStat.mtime) {
row.mi_modifyDate = jsFileStat.mtime
}
}
return true
}
function loadAllForms () {
let modelLastDate = new Date(App.globalCacheGet('UB_STATIC.modelsModifyDate')).getTime()
console.debug('modelLastDate = ', modelLastDate)
let models = App.domainInfo.models
let folders = []
if (!resultDataCache || modelLoadDate < modelLastDate) {
console.debug('load ubm_forms from models directory structure')
resultDataCache = []
for (let modelName in models) {
let model = models[modelName]
let mPath = path.join(model.realPublicPath, REL_PATH_TAIL)
folders.push({
path: mPath,
model: model // used for fill Document content for `mdb` store in postProcessing
})
}
let loader = new FileBasedStoreLoader({
entity: me.entity,
foldersConfig: folders,
fileMask: /-fm\.def$/,
onBeforeRowAdd: postProcessing
})
resultDataCache = loader.load()
modelLoadDate = modelLastDate
} else {
console.debug('ubm_form: resultDataCache already loaded')
}
return resultDataCache
}
/**
* Retrieve data from resultDataCache and init ctxt.dataStore
* caller MUST set dataStore.currentDataName before call doSelect
* @private
* @param {ubMethodParams} ctxt
*/
function doSelect (ctxt) {
let mP = ctxt.mParams
let aID = mP.ID
let cachedData = loadAllForms()
let cType = ctxt.dataStore.entity.cacheType
if (!(aID && (aID > -1)) && (cType === UBDomain.EntityCacheTypes.Entity || cType === UBDomain.EntityCacheTypes.SessionEntity) && (!mP.skipCache)) {
let reqVersion = mP.version
mP.version = resultDataCache.version
if (reqVersion === resultDataCache.version) {
mP.resultData = {}
mP.resultData.notModified = true
return
}
}
let filteredData = LocalDataStore.doFilterAndSort(cachedData, mP)
// return as asked in fieldList using compact format {fieldCount: 2, rowCount: 2, values: ["ID", "name", 1, "ss", 2, "dfd"]}
let resp = LocalDataStore.flatten(mP.fieldList, filteredData.resultData)
ctxt.dataStore.initFromJSON(resp)
}
/**
* @method select
* @memberOf ubm_form_ns.prototype
* @memberOfModule @unitybase/ubm
* @published
* @param {ubMethodParams} ctx
* @param {UBQL} ctx.mParams ORM query in UBQL format
* @return {Boolean}
*/
me.select = function (ctx) {
ctx.dataStore.currentDataName = 'select'
doSelect(ctx)
return true
}
/**
* Check form code start from form entity code and entity exist in domain. throw exception on fail
* @private
* @param {Number} aID
* @param {String} formCode
* @param {String} formEntity
*/
function validateInput (aID, formCode, formEntity) {
if (!App.domainInfo.has(formEntity)) {
throw new Error(`<<<entity "${formEntity}" not exist in Domain>>>`)
}
if (!aID) throw new Error('No ID parameter passed in execParams')
if (formCode.length < formEntity.length ||
(formCode.length === formEntity.length && formCode !== formEntity) ||
(formCode.length !== formEntity.length && formCode.substring(0, formEntity.length + 1) !== formEntity + '-')
) {
throw new Error(`<<<Invalid form code format. Must be "${formEntity}-FormVersion" where FormVersion is any character string>>>`)
}
let theSameCode = LocalDataStore.doFilterAndSort(resultDataCache, {whereList: {
byID: {expression: 'ID', condition: 'notEqual', values: {ID: aID}},
byCode: {expression: 'code', condition: 'equal', values: {code: formCode}}
}})
if (theSameCode.total !== 0) {
throw new Error('<<<Form with code "' + formCode + '" already exist>>>')
}
}
/**
* Return form body template from UBM/_templates/fileName is any or defaultBody
* @private
* @param {String} fileName
* @param {String} [defaultBody]
*/
function getFormBodyTpl (fileName, defaultBody) {
let filePath = path.join(App.domainInfo.models['UBM'].realPath, '_templates', fileName)
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : defaultBody
}
/**
* @private
* @param {ubMethodParams} ctxt
* @param {Object} storedValue
* @param {boolean} isInsert
* @return {boolean}
*/
function doUpdateInsert (ctxt, storedValue, isInsert) {
console.debug('--==== ubm_forms.doUpdateInsert ===-')
let entity = me.entity
let mP = ctxt.mParams
let newValues = mP.execParams || {}
let ID = newValues.ID
// move all attributes from execParams to storedValue
_.forEach(newValues, function (val, key) {
let attr = entity.attributes[key]
if (attr && (attr.dataType !== UBDomain.ubDataTypes.Document)) {
storedValue[key] = val
}
})
if (isInsert) {
ID = ncrc32(0, storedValue.code)
storedValue.ID = ID
}
let formEntity = App.domainInfo.get(storedValue.entity)
let newFormDefMeta = newValues.formDef
let newFormCodeMeta = newValues.formCode
let codeOfModelToStore = storedValue.model || formEntity.modelName
let formDefBody
let formScriptBody
if (isInsert) { // for insert operation create a boilerplate for form definition & form script
if (storedValue.formType === 'auto') {
formDefBody = getFormBodyTpl('new_autoFormDef.mustache', 'exports.formDef = {\r\n\titems:[\r\n\t\t/*put your items here*/\r\n\t]\r\n};')
} else if (storedValue.formType === 'custom') {
let codeParts = storedValue.code.split('-')
let className = formEntity.modelName + '.' + (codeParts[1] ? codeParts[1] : 'BetterToSetFormCodeToEntity-ClassName')
formDefBody = getFormBodyTpl('new_customForm.mustache', '').replace('{{className}}', className)
} else if (storedValue.formType === 'vue') {
formDefBody = getFormBodyTpl('new_vueFormDef.mustache', '')
}
// and for form script
if (storedValue.formType === 'auto') {
formScriptBody = getFormBodyTpl('new_autoFormJS.mustache', 'exports.formCode = {\r\n}')
} else if (storedValue.formType === 'vue') {
formScriptBody = getFormBodyTpl('new_vueFormJS.mustache', 'exports.formCode = {\r\n\tinitUBComponent: function () {\r\n\r\n\t}\r\n}')
}
if (formScriptBody) {
let formCodeInfo = blobStores.putContent({
entity: entity.name,
attribute: 'formCode',
ID: ID,
fileName: storedValue.code + JS_FILE_TAIL,
relPath: codeOfModelToStore + '|' + REL_PATH_TAIL
}, formScriptBody)
newFormCodeMeta = JSON.stringify(formCodeInfo)
}
} else { // for update operation load form definition body from store (temp or persistent if not modified)
if (newFormDefMeta) { // if form definition modified
formDefBody = blobStores.getContent({
entity: entity.name,
attribute: 'formDef',
ID: ID,
isDirty: true
}, {encoding: 'utf-8'})
} else {
formDefBody = blobStores.getContent({
entity: entity.name,
attribute: 'formDef',
ID: ID,
isDirty: false
}, {encoding: 'utf-8'}) //, JSON.parse(storedValue.formDef),
}
}
// replace comments in the beginning of form definition to the new one
let clearAttrReg = /^\/\/[ \t]?@(.+) "(.*)"[ \t]*\r?\n/gm // seek for //@ "bla bla" CRLF
formDefBody = formDefBody.replace(clearAttrReg, '') // remove all old entity attributes
let addedAttr = ''
for (let attrCode in entity.attributes) {
let attr = entity.attributes[attrCode]
if (attr.dataType !== UBDomain.ubDataTypes.Document && attr.defaultView && attrCode !== 'ID' && attrCode !== 'code') {
addedAttr = '// @' + attrCode + ' "' + storedValue[attrCode] + '"\r\n' + addedAttr
}
}
formDefBody = '// @! "do not remove comments below unless you know what you do!"\r\n' + addedAttr + formDefBody
// save modified form definition to the temp store
let formDefInfo = blobStores.putContent({
entity: entity.name,
attribute: 'formDef',
ID: ID,
fileName: storedValue.code + DEF_FILE_TAIL
// ct.fName = storedValue.code + DEF_FILE_TAIL
// ct.relPath = codeOfModelToStore + '|' + REL_PATH_TAIL
// ct.ct = JSON_CONTENT_TYPE
}, formDefBody)
// add a relPath
formDefInfo.relPath = codeOfModelToStore + '|' + REL_PATH_TAIL
// and update an attribute value to the new blob info
storedValue.formDef = JSON.stringify(formDefInfo)
if (newFormCodeMeta) { // in case form script is modified add a relPath
let parsed = JSON.parse(newFormCodeMeta)
parsed.relPath = codeOfModelToStore + '|' + REL_PATH_TAIL
parsed.fName = storedValue.code + JS_FILE_TAIL
newFormCodeMeta = JSON.stringify(parsed)
storedValue.formCode = newFormCodeMeta
} else {
delete storedValue.formCode
}
// commit BLOB store changes
let fakeCtx = {
dataStore: null,
mParams: {
execParams: storedValue
}
}
ctxt.dataStore.commitBLOBStores(fakeCtx, isInsert === false)
ctxt.dataStore.initialize([storedValue])
console.debug('--== ubm_form: resultDataCache cleared ==--')
resultDataCache = null // drop cache. afterInsert call select and restore cache
return true
}
/**
* @method update
* @memberOf ubm_form_ns.prototype
* @memberOfModule @unitybase/ubm
* @published
* @param {ubMethodParams} ctxt
* @return {boolean}
*/
me.update = function (ctxt) {
let inParams = ctxt.mParams.execParams || {}
let ID = inParams.ID
console.debug('!!ubm_form.update-----------------')
let cachedData = loadAllForms()
let storedValue = LocalDataStore.byID(cachedData, ID)
if (storedValue.total !== 1) {
throw new Error('Record with ID=' + ID + 'not found')
}
storedValue = LocalDataStore.selectResultToArrayOfObjects(storedValue)[0]
validateInput(ID, inParams.code || storedValue.code, inParams.entity || storedValue.entity)
if (inParams.code && inParams.code !== storedValue.code) {
throw new Error('<<<To change form code rename both *.def & *.js files & change "//@code "formCode" comment inside new def file>>>')
}
doUpdateInsert(ctxt, storedValue, false)
return true
}
/**
* Check ID is unique and perform insertion
* @method insert
* @memberOf ubm_form_ns.prototype
* @memberOfModule @unitybase/ubm
* @published
* @param {ubMethodParams} ctxt
* @return {boolean}
*/
me.insert = function (ctxt) {
let inParams = ctxt.mParams.execParams
let aID = inParams.ID
console.debug('--====== ubm_form.insert ====--')
let cachedData = loadAllForms()
validateInput(aID, inParams.code, inParams.entity)
let row = LocalDataStore.byID(cachedData, aID)
if (row.total) {
throw new UB.UBAbort(`<<<Form with ID ${aID} already exist in domain>>>`)
}
let oldValue = {}
doUpdateInsert(ctxt, oldValue, true)
return true
}
/**
* New form
* @method addNew
* @memberOf ubm_form_ns.prototype
* @memberOfModule @unitybase/ubm
* @published
* @param {ubMethodParams} ctxt
* @return {boolean}
*/
me.addnew = mStorage.addNew