UNPKG

apemandb

Version:
496 lines (466 loc) 13.3 kB
/** * Create a database model. * @function createModel */ 'use strict' const createAttribute = require('./create_attribute') const resolveModel = require('./resolve_model') const deleteEmptyProps = require('../helpers/delete_empty_props') const { pluralize, singularize } = require('inflection') const uuid = require('uuid') const { pascalcase, snakecase, camelcase } = require('stringcase') /** @lends createModel */ function createModel (db, config) { let { modelSearchPath = [] } = db config = createModel.formatConfig(config) config = createModel.applyInheritance(config, { modelSearchPath }) deleteEmptyProps(config) let { $name, $description, $abstract = false, $attributes = {}, $indices = [] } = config if ($abstract) { return {} } let name = createModel.parseName($name) let attributes = createModel.parseAttributes($attributes) let options = createModel.parseOptions($name, $description, $attributes, $indices) let model = db.define(name, attributes, options) Object.assign(model, config, createModel.mixins, { db }) return model } Object.assign(createModel, { /** * Reserved attributes. */ reservedAttributes: { vr: { $comment: 'Version number', $type: 'INTEGER', $default: 1 }, uuid: { $comment: 'Universal identifier', $type: 'UUID', $unique: true, $validate: { $isUUID: 4 }, $default: () => uuid.v4() }, createdAt: { $comment: 'Date of data created.', $type: 'DATE', $nullable: true }, updatedAt: { $comment: 'Date of data updated.', $type: 'DATE', $nullable: true } }, formatConfig (config) { let { $attributes } = config if ($attributes) { } else { $attributes = {} } let attributeNames = Object.keys(config || {}) .filter((name) => /^\$/.test(config)) for (let name of attributeNames) { $attributes[ name ] = config[ name ] delete config[ name ] } return Object.assign(config, { $attributes }) }, /** * Apply inheritance. * @param {Object} config * @param {Object} [options={}] * @returns {*} */ applyInheritance (config, options = {}) { let { modelSearchPath } = options return createModel.parseInheritance(config.$inherits || [], { modelSearchPath }) .map(($inherit) => createModel.applyInheritance($inherit, { modelSearchPath })) .reduce( (config, $inherit) => Object.assign( { $$inherited: Object.assign( config.$$inherited || {}, { [$inherit.$name]: $inherit.$$src || true } ) }, [ '$attributes', '$belongsTo', '$hasOne', '$hasMany', '$belongsToMany', '$indices' ].reduce((inherited, name) => Object.assign(inherited, { [name]: Object.assign({}, $inherit[ name ], config[ name ]) }), config) ) , config) }, /** * Parse inheritance. * @param {Object} $inherits - Inheritance * @param {Object} [options={}] - Optional settings * @returns {Array} */ parseInheritance ($inherits, options = {}) { let { modelSearchPath } = options return [].concat($inherits || []).map(($inherit) => { if (typeof $inherit === 'string') { let resolved = resolveModel($inherit, { modelSearchPath }) if (resolved) { return resolved } else { throw new Error(`[apemandb] Unable to resolve inheritance: ${$inherit}`) } } return $inherit }) }, /** * Parse model name. * @param {string} $name * @returns {string} */ parseName ($name) { return pascalcase($name).trim() }, /** * Parse model attributes. * @param {Object} $attributes * @returns {Object} */ parseAttributes ($attributes) { if (!$attributes) { console.warn('$attributes not found') return {} } let reserved = createModel.reservedAttributes for (let name of Object.keys(reserved)) { let conflict = $attributes.hasOwnProperty(name) if (conflict) { throw new Error(`[apemandb] You can not use ${name} as attribute since it is reserved.`) } } let defined = Object.assign({}, $attributes, reserved) return Object.keys(defined) .map((name) => Object.assign(defined[ name ], { $name: name })) .reduce((result, $attribute) => Object.assign(result, { [$attribute.$name]: createAttribute($attribute) }) , {}) }, /** * Parse model options. * @param {string} $name * @param {string} $description * @param {Object} $attributes * @param {Array} $indices * @returns { {} } */ parseOptions ($name, $description, $attributes, $indices) { let attributes = Object.keys($attributes || {}) .map((name) => Object.assign({ $name: name }, $attributes[ name ])) let dynamicAttributes = attributes .filter((attribute) => !!attribute.$dynamic) let pluralName = pluralize($name) let singularName = singularize($name) return { comment: $description, timestamps: false, underscored: true, name: { singular: singularName, plural: pluralName }, tableName: snakecase(pluralName), classMethods: { /** * Get plural name. * @returns {string} - Name */ getPluralName () { return pluralName }, /** * Get singular name. * @returns {string} - Name. */ getSingularName () { return singularName }, /** * Get database instance. * @returns {Apemandb} - The instance. */ getDB () { const s = this return s.db } }, hooks: { beforeCreate (instance, options = {}){ instance.createdAt = new Date() instance.updatedAt = new Date() }, beforeUpdate (instance, options = {}) { instance.vr += 1 instance.updatedAt = new Date() } }, getterMethods: dynamicAttributes .filter((attribute) => !!attribute.$get) .reduce((methods, attribute) => { let { $get, $name } = attribute // noinspection Eslint let getter = new Function( `return ${String($get || 'null').trim().replace(/^return/, '') }` ) return Object.assign(methods, { [$name]: getter }) }, {}), setterMethods: dynamicAttributes .filter((attribute) => !!(attribute.$readonly || attribute.$set)) .reduce((methods, attribute) => { let { $readonly, $set, $name } = attribute if ($readonly && $set) { throw new Error(`Failed define setter for ${$name}. You can not set "$readonly" and "$set" same time.`) } let setter if ($readonly) { setter = function readonlySetter () { throw new Error(`${$name} is readonly!`) } } else { // noinspection Eslint setter = new Function('value', $set) } return Object.assign(methods, { [$name]: setter }) }, {}), indexes: Object.keys($indices).map((fields) => { let $index = $indices[ fields ] if ($index === false) { return null } if ($index === true) { $index = {} } return Object.keys($index).reduce((index, key) => Object.assign(index, { [key.replace(/^\$/, '')]: index[ key ] }), { fields: fields.split(',').map((field) => snakecase(field)) }) }).filter(Boolean) } }, /** * Model mixin methods. */ mixins: { _refs: null, /** * Set referencing model. * @param {string} name - Name of the model * @param {Object} ref - Reference data. */ addRef (name, ref) { const s = this s._refs = s._refs || {} s._refs[ name ] = ref }, /** * Name foreign key points this model. * @returns {string} */ nameForeignKey (as) { const s = this return camelcase([ as || s.$name, s.primaryKeyAttribute ].join('_')) }, /** * Reference model * @returns {Object} */ getRefs () { const s = this return s._refs || {} }, /** * Get model names of refs * @param {Object} [options={}] - Optional settings * @returns {string[]} */ getRefModelNames (options = {}) { const s = this let refs = s.getRefs() let { relations } = options return Object.keys(refs).reduce((modelNames, key) => { let { model, $$relation: relation } = refs[ key ] if (relations) { let skip = !~relations.indexOf(relation) if (skip) { return modelNames } } return [ ...modelNames, model && model.name || key ] }, []) }, /** * Get a reference model. * @param {string} name * @returns {Object} - Referenced model. */ getRef (name) { const s = this let refs = s.getRefs() let model = refs[ name ] || refs[ pascalcase(name) ] if (!model) { throw new Error(`Unknown ref "${name}" (available: ${JSON.stringify(Object.keys(refs))})`) } return model }, /** * Define ref * @param {string} name * @param {Object} [values={}] * @returns {Object} */ ref (name, values = {}) { const s = this return Object.assign({}, s.getRef(name), values) }, /** * Call super method. * @param {string} name - Name of the method. * @param {Array} args - Apply arguments. * @returns {*} - Apply result. */ applySuper (name, args) { const s = this // noinspection Eslint let method = s.__proto__[ name ] return method.apply(s, args) }, /** * Verify attribute name. * @param {string} name - Name of attribute to assert. * @param {Object} options */ assertAttributeName (name, options = {}) { const s = this let refs = s.getRefs() let attributes = s.attributes let isKnown = attributes.hasOwnProperty(name) || attributes.hasOwnProperty(camelcase(name)) || refs.hasOwnProperty(camelcase(name)) || refs.hasOwnProperty(name) || (!!~Object.keys(refs).map((name) => refs[ name ].as).indexOf(name)) || (options.attributes || []).indexOf(name) if (!isKnown) { console.warn(`[apemandb] Unknown attribute "${name}" passed to ${s.$name} model.`) } }, /** * Build values. * @param {Object} values * @param {Object} [options] * @returns {Object} */ build (values, options = {}) { const s = this for (let name of Object.keys(values || {})) { s.assertAttributeName(name, options) } return s.applySuper('build', arguments) }, /** * Add one-to-one relation on the source model. * @param {Object} model * @param {Object} options */ belongsTo (model, options = {}) { const s = this let { as } = options let $$relation = 'belongsTo' s.addRef(as, { model, as, $$relation }) return s.applySuper('belongsTo', arguments) }, /** * Add one-to-one relation on the target model. * @param {Object} model * @param {Object} options */ hasOne (model, options = {}) { const s = this let { as } = options let $$relation = 'hasOne' s.addRef(as, { model, as, $$relation }) return s.applySuper('hasOne', arguments) }, /** * Add one-to many relation on the target model. * @param {Object} model * @param {Object} options */ hasMany (model, options = {}) { const s = this let { as } = options let $$relation = 'hasMany' s.addRef(as, { model, as, $$relation }) return s.applySuper('hasMany', arguments) }, /** * Add one-to-many relation on the target model. * @param {Object} model * @param {Object} options */ belongsToMany (model, options = {}) { const s = this let { as, through, throughModel } = options let $$relation = 'belongsToMany' s.addRef(camelcase(through), { model, as, through: { as: camelcase(through), model: throughModel }, $$relation }) return s.applySuper('belongsToMany', arguments) }, /** * Find an instance with id. * @param {number|string} id * @param {Object} options - Optional settings * @returns {Promise} */ findById (id, options = {}) { const s = this if (typeof id === 'undefined') { return null } return s.applySuper('findById', arguments).then((found) => found || s.findOne( Object.assign({ where: { uuid: id } }, options) ) ) } } }) module.exports = createModel