dobo
Version:
DBMS for Bajo Framework
365 lines (350 loc) • 14.5 kB
JavaScript
import path from 'path'
import modelFactory from './factory/model.js'
import actionFactory from './factory/action.js'
/**
* Sanitize one single property of a model
*
* @param {Object} model - Loaded model
* @param {Object} prop - Property to check
* @param {Array} [indexes] - Container array to fill up found index
*/
async function sanitizeProp (model, prop, indexes) {
const { isEmpty, isString, keys, pick, isArray, isPlainObject, omit, isFunction } = this.app.lib._
const allPropKeys = this.getAllPropertyKeys(model.connection.driver)
const propType = this.constructor.propertyType
if (isString(prop)) {
let [name, type, maxLength, idx, required] = prop.split(',').map(m => m.trim())
if (isEmpty(type)) type = 'string'
maxLength = isEmpty(maxLength) ? propType.string.maxLength : parseInt(maxLength)
prop = { name, type, maxLength }
if (!isEmpty(idx)) prop.index = idx
prop.required = required === 'true'
}
prop.type = prop.type ?? 'string'
if (isArray(prop.values)) {
prop.values = prop.values.map(item => {
if (isPlainObject(item)) return pick(item, ['value', 'text'])
return { value: item, text: item }
})
} else if (!(isString(prop.values) || isFunction(prop.values))) delete prop.values
if (prop.hidden) model.hidden.push(prop.name)
if (prop.scanable) model.scanables.push(prop.scanable)
if (prop.virtual) {
const keys = Object.keys(propType)
if (!keys.includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}:${prop.type}`, model.name)
for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable']) {
delete prop[key]
}
model.properties.push(prop)
} else {
if (prop.index) {
if (prop.index === true || prop.index === 'true') prop.index = 'index'
const [idx, idxName] = prop.index.split(':')
const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
indexes.push(index)
}
if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
else {
const feature = this.getFeature(prop.type)
if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
const opts = omit(prop, ['name', 'type'])
opts.field = prop.name
await applyFeature.call(this, model, feature, opts, indexes)
}
}
}
/**
* Collect all properties it can be found on model
*
* @param {Object} model - Model
* @param {Array} [inputs] - Array of properties
* @param {Array} [indexes] - Container array to fill up found index
*/
async function findAllProps (model, inputs = [], indexes = []) {
const { isPlainObject, cloneDeep } = this.app.lib._
const isIdProp = inputs.find(p => {
return isPlainObject(p) ? p.name === 'id' : p.startsWith('id,')
})
if (!isIdProp) {
const idField = cloneDeep(model.connection.driver.idField)
idField.name = 'id'
inputs.unshift(idField)
}
for (const prop of inputs) {
await sanitizeProp.call(this, model, prop, indexes)
}
}
/**
* Apply each feature found
* @param {Object} model - Model
* @param {Object} feature - Feature to apply
* @param {Object} options - Options to the feature
* @returns {Array} New properties found in feature
*/
async function applyFeature (model, feature, options, indexes) {
const { isArray, findIndex } = this.app.lib._
if (feature.name === 'dobo:unique' && options.field === 'id') {
const idx = findIndex(model.properties, { name: 'id' })
if (idx > -1) model.properties.pullAt(idx)
}
const item = await feature.handler(options)
if (item.rules) model.rules.push(...item.rules)
if (item.scanables) model.scanables.push(...item.scanables)
if (!isArray(item.properties)) item.properties = [item.properties]
for (const prop of item.properties) {
prop.feature = `${feature.plugin.ns}:${feature.name}`
await sanitizeProp.call(this, model, prop, indexes)
}
if (item.hooks) {
item.hooks = item.hooks.map(hook => {
hook.level = hook.level ?? 999
return hook
})
model.hooks.push(...item.hooks)
}
}
/**
* Collect all features it can be found on model
*
* @param {Object} model - Model
* @param {Array} [inputs] - Array of properties
*/
async function findAllFeats (model, inputs = [], indexes = []) {
const { isString, omit } = this.app.lib._
for (let feat of inputs) {
if (isString(feat)) feat = { name: feat }
const featName = feat.name.indexOf(':') === -1 ? `dobo:${feat.name}` : feat.name
const feature = this.app.dobo.getFeature(featName)
if (!feature) this.fatal('invalidFeature%s%s', model.name, featName)
await applyFeature.call(this, model, feature, omit(feat, 'name'), indexes)
}
}
/**
* Collect all indexes it can be found on model
*
* @param {Object} model - Model
* @param {Array} [inputs] - Array of properties
*/
async function findAllIndexes (model, inputs = [], indexes = []) {
const items = []
for (const index of inputs) {
index.type = index.type ?? 'index'
index.fields = index.fields ?? []
if (!index.name) index.name = `${model.name}_${index.fields.join('_')}_${index.type}`
items.push(index)
}
model.indexes = [...items, ...indexes]
}
/**
* Sanitize any reference/relationship found in properties
*
* @param {Object} model - Model
* @param {Array} [models] - All model match agaist. Defaults to ```dobo.models```
*/
export async function sanitizeRef (model, models) {
const { find, isString, pullAt } = this.app.lib._
const _refKeys = []
for (const prop of model.properties) {
const ignored = []
for (const key in prop.ref ?? {}) {
_refKeys.push(key)
let ref = prop.ref[key]
if (isString(ref)) {
ref = { field: ref }
}
ref.field = ref.field ?? 'id'
ref.type = ref.type ?? '1:1'
ref.searchField = ref.searchField ?? ref.field
ref.labelField = ref.labelField ?? ref.searchField
const rModel = find(models, { name: ref.model })
if (!rModel) {
ignored.push(key)
this.log.warn('notFound%s%s', this.t('model'), ref.model)
continue
}
const rProp = find(rModel.properties, { name: ref.field })
if (!rProp) {
ignored.push(key)
this.log.warn('notFound%s%s', this.t('property'), `${ref.field}@${ref.model}`)
continue
}
ref.fields = ref.fields ?? '*'
if (['*', 'all'].includes(ref.fields)) ref.fields = rModel.properties.map(p => p.name)
if (ref.fields.length > 0 && !ref.fields.includes('id')) ref.fields.unshift('id')
const removed = []
for (const idx in ref.fields) {
const p = find(rModel.properties, { name: ref.fields[idx] })
if (!p) removed.push(ref.fields[idx])
}
pullAt(ref.fields, removed)
prop.ref[key] = ref
}
for (const key of ignored) {
delete prop.ref[key]
}
}
const dupes = _refKeys.filter((item, index) => _refKeys.indexOf(item) !== index)
if (dupes.length > 0) throw this.error('duplicateRefKeys%s%s', model.name, dupes.join(', '))
}
/**
* Sanitize all but reference, because a reference needs all models to be available first
*
* @param {Object} model - Model
*/
export async function sanitizeAll (model) {
const { runHook } = this.app.bajo
const { pick, keys, map, uniq, camelCase, filter } = this.app.lib._
const { defaultsDeep } = this.app.lib.aneka
const allPropNames = uniq(map(model.properties, 'name'))
const propType = this.constructor.propertyType
const indexTypes = this.constructor.indexTypes
await runHook(`dobo.${camelCase(model.name)}:beforeSanitizeModel`, model)
// properties
for (const idx in model.properties) {
let prop = model.properties[idx]
const def = propType[prop.type]
prop = pick(defaultsDeep(prop, def), this.getPropertyKeysByType(prop.type))
if (!keys(propType).includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}.${def.name}`, prop.type)
if (prop.type === 'string') {
prop.minLength = parseInt(prop.minLength) ?? 0
prop.maxLength = parseInt(prop.maxLength) ?? 255
if (prop.minLength > 0) prop.required = true
if (prop.minLength === 0) delete prop.minLength
}
model.properties[idx] = prop
}
// indexes
for (const index of model.indexes) {
if (!indexTypes.includes(index.type)) this.fatal('unknownIndexType%s%s', index.type, model.name)
for (const field of index.fields) {
if (!allPropNames.includes(field)) this.fatal('unknownPropNameOnIndex%s%s', field, model.name)
}
}
await runHook(`dobo.${camelCase(model.name)}:afterSanitizeModel`, model)
// sorting only possible using these fields
model.sortables = []
for (const index of model.indexes) {
model.sortables.push(...index.fields)
}
model.hidden = filter(uniq(model.hidden), prop => allPropNames.includes(prop))
}
/**
* Create schema for model
*
* @param {Object} item - Source item
* @returns {Object} Sanitized model
*/
async function createSchema (item) {
const { find, orderBy, get } = this.app.lib._
const { mergeObjectsByKey, defaultsDeep, parseObject } = this.app.lib.aneka
const feats = item.features ?? []
const props = item.properties ?? []
const indexes = item.indexes ?? []
item.features = []
item.properties = []
item.indexes = []
item.hidden = item.hidden ?? []
item.rules = item.rules ?? []
item.buildLevel = item.buildLevel ?? 999
item.hooks = item.hooks ?? []
item.disabled = item.disabled ?? []
item.scanables = item.scanables ?? []
if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
const conn = item.connection ?? 'default'
if (!(conn instanceof this.app.baseClass.DoboConnection)) {
item.connection = null
// Is there any overwritten connection?
const newConn = find(this.connections, c => c.options.models.includes(item.name))
if (newConn) item.connection = newConn
else {
item.connection = this.getConnection(conn, true)
if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
}
if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
}
// cache settings
const defCache = defaultsDeep({}, item.connection.options.cache, get(this, 'app.bajoCache.config.default', this.config.default.cache))
if (item.cache === false) item.cache = { ttlDur: 0 }
else if (item.cache === true) item.cache = defCache
else item.cache = defaultsDeep({}, item.cache, defCache)
item.cache = parseObject(item.cache)
if (item.connection.name === 'memory') item.cache.ttlDur = 0
// let's run
await findAllProps.call(this, item, props, indexes)
await findAllFeats.call(this, item, feats, indexes)
await findAllIndexes.call(this, item, indexes)
for (const key of ['properties', 'indexes']) {
item[key] = mergeObjectsByKey(item[key], 'name')
}
item.hooks = orderBy(item.hooks, ['name', 'level'])
delete item.features
await sanitizeAll.call(this, item)
return item
}
/**
* Collect all models from loaded plugins and create the models
*
* @name collectModels
* @memberof module:lib
* @async
* @see Dobo#init
*/
async function collectModels () {
const { eachPlugins, callHandler } = this.app.bajo
const { orderBy, has, omit } = this.app.lib._
await actionFactory.call(this)
await modelFactory.call(this)
this.log.trace('collecting%s', this.t('model'))
const me = this
let schemas = []
await eachPlugins(async function ({ file }) {
const { readConfig, callHandler } = this.app.bajo
const { pascalCase } = this.app.lib.aneka
const { isPlainObject, isEmpty, isArray } = this.app.lib._
let items = await readConfig(file, { ns: this.ns, baseNs: me.ns, merge: true })
if (isEmpty(items)) return undefined
if (isPlainObject(items)) {
items.baseName = items.baseName ?? path.basename(file, path.extname(file))
items.name = items.name ?? pascalCase(`${this.alias} ${items.baseName}`)
}
if (!isArray(items)) items = [items]
for (const item of items) {
if (!item.baseName) me.fatal('missing%s%s', 'baseName', file)
item.name = item.name ?? pascalCase(`${this.alias} ${item.baseName}`)
me.log.trace('- %s', item.name)
item.collName = item.collName ?? item.name
item.options = item.options ?? {}
item.options.attachment = item.options.attachment ?? true
item.options.persistence = item.options.persistence ?? true
item.ns = this.ns
if (item.buildStart) await callHandler(this, item.buildStart, item)
const schema = await createSchema.call(me, item)
schemas.push(schema)
}
}, { glob: ['model/*.*', 'model.*'], prefix: this.ns })
schemas = orderBy(schemas, ['buildLevel', 'name'])
for (const schema of schemas) {
const idProp = schema.properties.find(p => p.name === 'id')
if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
const plugin = this.app[schema.ns]
await sanitizeRef.call(this, schema, schemas)
for (const item of schema.indexes) {
for (const field of item.fields) {
const prop = schema.properties.find(p => p.name === field)
if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index', schema.name)
}
}
for (const field of schema.scanables) {
const prop = schema.properties.find(p => p.name === field)
if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', schema.name)
}
const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
me.models.push(model)
if (schema.buildEnd) await callHandler(plugin, schema.buildEnd, model)
}
schemas = []
this.log.debug('collected%s%d', this.t('model'), this.models.length)
}
export default collectModels