apemandb
Version:
Database for apeman project.
496 lines (466 loc) • 13.3 kB
JavaScript
/**
* Create a database model.
* @function createModel
*/
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