UNPKG

lux-framework

Version:

Build scalable, Node.js-powered REST APIs with almost no code.

476 lines (413 loc) 10.2 kB
// @flow import { camelize, dasherize, pluralize, singularize } from 'inflection'; import { line } from '../../logger'; import { createAttribute } from '../attribute'; import { get as getRelationship, set as setRelationship } from '../relationship'; import entries from '../../../utils/entries'; import underscore from '../../../utils/underscore'; import type Database, { Model } from '../index'; // eslint-disable-line no-unused-vars, max-len const VALID_HOOKS = new Set([ 'afterCreate', 'afterDestroy', 'afterSave', 'afterUpdate', 'afterValidation', 'beforeCreate', 'beforeDestroy', 'beforeSave', 'beforeUpdate', 'beforeValidation' ]); /** * @private */ function initializeProps(prototype, attributes, relationships) { Object.defineProperties(prototype, { ...entries(attributes).reduce((obj, [key, value]) => ({ ...obj, [key]: createAttribute({ key, ...value }) }), {}), ...Object.keys(relationships).reduce((obj, key) => ({ ...obj, [key]: { get() { return getRelationship(this, key); }, set(val) { setRelationship(this, key, val); } } }), {}) }); } /** * @private */ function initializeHooks({ model, hooks, logger }) { return Object.freeze( entries(hooks).reduce((obj, [key, value]) => { if (!VALID_HOOKS.has(key)) { logger.warn(line` Invalid hook '${key}' will not be added to Model '${model.name}'. Valid hooks are ${ Array.from(VALID_HOOKS).map(h => `'${h}'`).join(', ') }. `); return obj; } return { ...obj, [key]: async (instance, transaction) => { await Reflect.apply(value, model, [instance, transaction]); } }; }, {}) ); } /** * @private */ function initializeValidations(opts) { const { model, logger, attributes } = opts; const attributeNames = Object.keys(attributes); let { validates } = opts; validates = entries(validates) .filter(([key, value]) => { let isValid = attributeNames.indexOf(key) >= 0; if (!isValid) { logger.warn(line` Invalid validation '${key}' will not be added to Model '${model.name}'. '${key}' is not an attribute of Model '${model.name}'. `); } if (typeof value !== 'function') { isValid = false; logger.warn(line` Invalid validation '${key}' will not be added to Model '${model.name}'. Validations must be a function. `); } return isValid; }) .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); return Object.freeze(validates); } /** * @private */ export default async function initializeClass<T: Class<Model>>({ store, table, model }: { store: Database, table: $PropertyType<T, 'table'>, model: T }): Promise<T> { let { hooks, scopes, validates } = model; const { logger } = store; const modelName = dasherize(underscore(model.name)); const resourceName = pluralize(modelName); const attributes = entries(await table().columnInfo()) .reduce((obj, [columnName, value]) => ({ ...obj, [camelize(columnName, true)]: { ...value, columnName, docName: dasherize(columnName) } }), {}); const belongsTo = entries(model.belongsTo || {}) .reduce((obj, [relatedName, { inverse, model: relatedModel }]) => { const relationship = {}; Object.defineProperties(relationship, { model: { value: store.modelFor(relatedModel || relatedName), writable: false, enumerable: true, configurable: false }, inverse: { value: inverse, writable: false, enumerable: true, configurable: false }, type: { value: 'belongsTo', writable: false, enumerable: false, configurable: false }, foreignKey: { value: `${underscore(relatedName)}_id`, writable: false, enumerable: false, configurable: false } }); return { ...obj, [relatedName]: relationship }; }, {}); const hasOne = entries(model.hasOne || {}) .reduce((obj, [relatedName, { inverse, model: relatedModel }]) => { const relationship = {}; Object.defineProperties(relationship, { model: { value: store.modelFor(relatedModel || relatedName), writable: false, enumerable: true, configurable: false }, inverse: { value: inverse, writable: false, enumerable: true, configurable: false }, type: { value: 'hasOne', writable: false, enumerable: false, configurable: false }, foreignKey: { value: `${underscore(inverse)}_id`, writable: false, enumerable: false, configurable: false } }); return { ...obj, [relatedName]: relationship }; }, {}); const hasMany = entries(model.hasMany || {}) .reduce((hash, [relatedName, opts]) => { const { inverse } = opts; const relationship = {}; let { through, model: relatedModel } = opts; let foreignKey; if (typeof relatedModel === 'string') { relatedModel = store.modelFor(relatedModel); } else { relatedModel = store.modelFor(relatedName); } if (typeof through === 'string') { through = store.modelFor(through); foreignKey = `${singularize(underscore(inverse))}_id`; } else { foreignKey = `${underscore(inverse)}_id`; } Object.defineProperties(relationship, { model: { value: relatedModel, writable: false, enumerable: true, configurable: false }, inverse: { value: inverse, writable: false, enumerable: true, configurable: false }, through: { value: through, writable: false, enumerable: Boolean(through), configurable: false }, type: { value: 'hasMany', writable: false, enumerable: false, configurable: false }, foreignKey: { value: foreignKey, writable: false, enumerable: false, configurable: false } }); return { ...hash, [relatedName]: relationship }; }, {}); Object.freeze(hasOne); Object.freeze(hasMany); Object.freeze(belongsTo); const relationships = Object.freeze({ ...hasOne, ...hasMany, ...belongsTo }); if (!hooks) { hooks = {}; } if (!scopes) { scopes = {}; } if (!validates) { validates = {}; } Object.defineProperties(model, { store: { value: store, writable: false, enumerable: false, configurable: false }, table: { value: table, writable: false, enumerable: false, configurable: false }, logger: { value: logger, writable: false, enumerable: false, configurable: false }, attributes: { value: Object.freeze(attributes), writable: false, enumerable: false, configurable: false }, attributeNames: { value: Object.freeze(Object.keys(attributes)), writable: false, enumerable: false, configurable: false }, hasOne: { value: hasOne, writable: false, enumerable: Boolean(Object.keys(hasOne).length), configurable: false }, hasMany: { value: hasMany, writable: false, enumerable: Boolean(Object.keys(hasMany).length), configurable: false }, belongsTo: { value: belongsTo, writable: false, enumerable: Boolean(Object.keys(belongsTo).length), configurable: false }, relationships: { value: relationships, writable: false, enumerable: false, configurable: false }, relationshipNames: { value: Object.freeze(Object.keys(relationships)), writable: false, enumerable: false, configurable: false }, hooks: { value: initializeHooks({ model, hooks, logger }), writable: false, enumerable: Boolean(Object.keys(hooks).length), configurable: false }, scopes: { value: scopes, writable: false, enumerable: Boolean(Object.keys(scopes).length), configurable: false }, validates: { value: initializeValidations({ model, logger, validates, attributes }), writable: false, enumerable: Boolean(Object.keys(validates).length), configurable: false }, modelName: { value: modelName, writable: false, enumerable: true, configurable: false }, resourceName: { value: resourceName, writable: false, enumerable: true, configurable: false }, initialized: { value: true, writable: false, enumerable: false, configurable: false }, ...Object.freeze( entries(scopes).reduce((obj, [name, scope]) => ({ ...obj, [name]: { value: scope, writable: false, enumerable: false, configurable: false } }), {}) ) }); initializeProps(model.prototype, attributes, { ...hasOne, ...hasMany, ...belongsTo }); Object.defineProperties(model.prototype, { modelName: { value: modelName, writable: false, enumerable: true, configurable: false }, resourceName: { value: resourceName, writable: false, enumerable: true, configurable: false }, isModelInstance: { value: true, writable: false, enumerable: false, configurable: false } }); return model; }