UNPKG

dobo

Version:

DBMS for Bajo Framework

365 lines (350 loc) 14.5 kB
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