ubm/ubm_diagram.js

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 UB = require('@unitybase/ub')
const App = UB.App
const blobStores = App.blobStores
const mStorage = UB.mixins.mStorage

/* global ubm_diagram ncrc32 */
// eslint-disable-next-line camelcase
let me = ubm_diagram

const DIAGRAM_CONTENT_TYPE = 'application/ubMetaDiagram'
const REL_PATH_TAIL = 'erdiagrams'
const XML_EXTENSION = '.xml'

me.entity.addMethod('select')
me.entity.addMethod('update')
me.entity.addMethod('insert')
me.entity.addMethod('addnew')

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) {
  // we fill relPath in form "modelName"|"path inside model public folder" as expected by mdb virtual store
  let relPath = loader.processingRootFolder.model.name + '|' + REL_PATH_TAIL

  // fill model attribute by current folder model name
  row.model = loader.processingRootFolder.model.name

  // fill name attribute with file name w/o ".xml" extension
  let fileName = path.basename(fullFilePath)
  row.name = fileName.substring(0, fileName.length - XML_EXTENSION.length)

  if (row.ID) console.warn(`Please, remove a row "//@ID ${row.ID}" from a file ${fileName}. In UB5 ER diagram ID is generated automatically as crc32(name)`)
  row.ID = ncrc32(0, row.name)

  // fill formDef attribute value
  row.document = JSON.stringify({
    fName: fileName,
    origName: fileName,
    ct: DIAGRAM_CONTENT_TYPE,
    size: content.length,
    md5: 'fakemd50000000000000000000000000',
    relPath: relPath
  })
  return true
}

function loadAllDiagrams () {
  let models = App.domainInfo.models
  let folders = []
  let modelLastDate = new Date(App.globalCacheGet('UB_STATIC.modelsModifyDate')).getTime()

  console.debug('modelLastDate = ', modelLastDate)
  if (!resultDataCache || modelLoadDate < modelLastDate) {
    console.debug('load diagrams 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: new RegExp(XML_EXTENSION + '$'),
      attributeRegExpString: FileBasedStoreLoader.XML_ATTRIBURE_REGEXP,
      onBeforeRowAdd: postProcessing
    })
    resultDataCache = loader.load()

    modelLoadDate = modelLastDate
  } else {
    console.debug('ubm_diagram: 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 cType = ctxt.dataStore.entity.cacheType

  let cachedData = loadAllDiagrams()

  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_diagram_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 model exists
 * @private
 * @param {Number} aID
 * @param {String} modelName
 * @param {string} name
 */
function validateInput (aID, modelName, name) {
  let model = App.domainInfo.models[modelName]
  if (!model) {
    throw new Error(`ubm_diagram: Invalid model attribute value "${modelName}". Model not exist in domain`)
  }
  if (!name) throw new Error('ubm_diagram: name is required')
}

/**
 * @private
 * @param {ubMethodParams} ctxt
 * @param {Object} storedValue
 * @param {Boolean} isInsert
 * @return {boolean}
 */
function doUpdateInsert (ctxt, storedValue, isInsert) {
  console.debug('--==== ubm_diagram.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
    }
  })
  let newDocument = newValues.document
  let diagramBody
  if (isInsert || !newDocument) {
    diagramBody = `<mxGraphModel><root></root></mxGraphModel>`
  } else {
    diagramBody = blobStores.getContent(
      {
        entity: entity.name,
        attribute: 'document',
        ID: ID,
        isDirty: Boolean(newDocument)
      },
      {encoding: 'utf-8'}
    )
    let clearAttrReg = new RegExp(FileBasedStoreLoader.XML_ATTRIBURE_REGEXP, 'gm') // seek for <!--@attr "bla bla"-->CRLF
    diagramBody = diagramBody.replace(clearAttrReg, '') // remove all old entity attributes
  }
  let docInfo = blobStores.putContent({
    entity: entity.name,
    attribute: 'document',
    ID: ID,
    fileName: storedValue.name + XML_EXTENSION,
    ct: DIAGRAM_CONTENT_TYPE
  }, diagramBody)
  // add a relPath
  docInfo.relPath = storedValue.model + '|' + REL_PATH_TAIL
  // and update an attribute value to the new blob info
  storedValue.document = JSON.stringify(docInfo)

  // commit BLOB store changes
  let fakeCtx = {
    dataStore: null,
    mParams: {
      execParams: storedValue
    }
  }
  ctxt.dataStore.commitBLOBStores(fakeCtx, isInsert === false)
  ctxt.dataStore.initialize([storedValue])

  resultDataCache = null // drop cache. afterInsert call select and restore cache
  return true
}

/**
 * @method update
 * @memberOf ubm_diagram_ns.prototype
 * @memberOfModule @unitybase/ubm
 * @published
 * @param {ubMethodParams} ctxt
 * @return {boolean}
 */
me.update = function (ctxt) {
  let inParams = ctxt.mParams.execParams || {}
  let ID = inParams.ID
  let cachedData = loadAllDiagrams()
  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.model || storedValue.model, inParams.name || storedValue.name)
  doUpdateInsert(ctxt, storedValue, false)
  return true
}

/**
 * Check ID is unique and perform insertion
 * @method insert
 * @memberOf ubm_diagram_ns.prototype
 * @memberOfModule @unitybase/ubm
 * @published
 * @param {ubMethodParams} ctxt
 * @return {boolean}
 */
me.insert = function (ctxt) {
  let inParams = ctxt.mParams.execParams
  let newName = inParams.name
  let ID = ncrc32(0, newName)
  inParams.ID = ID
  validateInput(ID, inParams.model, newName)

  let cachedData = loadAllDiagrams()

  let row = LocalDataStore.byID(cachedData, ID)
  if (row.total) {
    throw new Error(`Diagram with name ${newName} already exist`)
  }
  let oldValue = {}
  doUpdateInsert(ctxt, oldValue, true)
  return true // everything is OK
}
/**
 * New diagram
 * @method addNew
 * @memberOf ubm_form_ns.prototype
 * @memberOfModule @unitybase/ubm
 * @published
 * @param {ubMethodParams} ctxt
 * @return {boolean}
 */
me.addnew = mStorage.addNew