@adonisjs/lucid
Version:
- [x] Paginate method - [x] forPage method - [ ] chunk ( removed ) - [ ] pluckAll ( removed ) - [x] withPrefix - [x] transactions - [x] global transactions
1,184 lines (1,093 loc) • 26.9 kB
JavaScript
'use strict'
/*
* adonis-lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
const _ = require('lodash')
const moment = require('moment')
const { resolver } = require('../../../lib/iocResolver')
const BaseModel = require('./Base')
const Hooks = require('../Hooks')
const QueryBuilder = require('../QueryBuilder')
const EagerLoad = require('../EagerLoad')
const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough } = require('../Relations')
const CE = require('../../Exceptions')
const util = require('../../../lib/util')
/**
* Lucid model is a base model and supposed to be
* extended by other models.
*
* @binding Adonis/Src/Model
* @alias Model
* @group Database
*
* @class Model
*/
class Model extends BaseModel {
/**
* Boot model if not booted. This method is supposed
* to be executed via IoC container hooks.
*
* @method _bootIfNotBooted
*
* @return {void}
*
* @private
*
* @static
*/
static _bootIfNotBooted () {
if (!this.$booted) {
this.$booted = true
this.boot()
}
}
/**
* An array of methods to be called everytime
* a model is imported via ioc container.
*
* @attribute iocHooks
*
* @return {Array}
*
* @static
*/
static get iocHooks () {
return ['_bootIfNotBooted']
}
/**
* The primary key for the model. You can change it
* to anything you want, just make sure that the
* value of this key will always be unique.
*
* @attribute primaryKey
*
* @return {String} The default value is `id`
*
* @static
*/
static get primaryKey () {
return 'id'
}
/**
* The foreign key for the model. It is generated
* by converting model name to lowercase and then
* snake case and appending `_id` to it.
*
* @attribute foreignKey
*
* @return {String}
*
* @example
* ```
* User - user_id
* Post - post_id
* ``
*/
static get foreignKey () {
return util.makeForeignKey(this.name)
}
/**
* Tell Lucid whether primary key is supposed to be incrementing
* or not. If `false` is returned then you are responsible for
* setting the `primaryKeyValue` for the model instance.
*
* @attribute incrementing
*
* @return {Boolean}
*
* @static
*/
static get incrementing () {
return true
}
/**
* Returns the value of primary key regardless of
* the key name.
*
* @attribute primaryKeyValue
*
* @return {Mixed}
*/
get primaryKeyValue () {
return this.$attributes[this.constructor.primaryKey]
}
/**
* Override primary key value.
*
* Note: You should know what you are doing, since primary
* keys are supposed to be fetched automatically from
* the database table.
*
* The only time you want to do is when `incrementing` is
* set to false
*
* @attribute primaryKeyValue
*
* @param {Mixed} value
*
* @return {void}
*/
set primaryKeyValue (value) {
this.$attributes[this.constructor.primaryKey] = value
}
/**
* The table name for the model. It is dynamically generated
* from the Model name by pluralizing it and converting it
* to lowercase.
*
* @attribute table
*
* @return {String}
*
* @static
*
* @example
* ```
* Model - User
* table - users
*
* Model - Person
* table - people
* ```
*/
static get table () {
return util.makeTableName(this.name)
}
/**
* Get fresh instance of query builder for
* this model.
*
* @method query
*
* @return {LucidQueryBuilder}
*
* @static
*/
static query () {
const query = new (this.QueryBuilder || QueryBuilder)(this, this.connection)
/**
* Listening for query event and executing
* listeners if any
*/
query.on('query', (builder) => {
_(this.$queryListeners)
.filter((listener) => typeof (listener) === 'function')
.each((listener) => listener(builder))
})
return query
}
/**
* Method to be called only once to boot
* the model.
*
* NOTE: This is called automatically by the IoC
* container hooks when you make use of `use()`
* method.
*
* @method boot
*
* @return {void}
*
* @static
*/
static boot () {
this.hydrate()
_.each(this.traits, (trait) => this.addTrait(trait))
}
/**
* Hydrates model static properties by re-setting
* them to their original value.
*
* @method hydrate
*
* @return {void}
*
* @static
*/
static hydrate () {
/**
* Model hooks for different lifecycle
* events
*
* @type {Object}
*/
this.$hooks = {
before: new Hooks(),
after: new Hooks()
}
/**
* List of global query listeners for the model.
*
* @type {Array}
*/
this.$queryListeners = []
/**
* List of global query scopes. Chained before executing
* query builder queries.
*/
this.$globalScopes = []
/**
* We use the default query builder class to run queries, but as soon
* as someone wants to add methods to the query builder via traits,
* we need an isolated copy of query builder class just for that
* model, so that the methods added via traits are not impacting
* other models.
*/
this.QueryBuilder = null
}
/**
* Define a query macro to be added to query builder.
*
* @method queryMacro
*
* @param {String} name
* @param {Function} fn
*
* @chainable
*/
static queryMacro (name, fn) {
/**
* Someone wished to add methods to query builder but just for
* this model. First get a unique copy of query builder and
* then add methods to it's prototype.
*/
if (!this.QueryBuilder) {
this.QueryBuilder = class ExtendedQueryBuilder extends QueryBuilder {}
}
this.QueryBuilder.prototype[name] = fn
return this
}
/**
* Adds a new hook for a given event type.
*
* @method addHook
*
* @param {String} forEvent
* @param {Function|String} handler
*
* @return {void}
*
* @static
*/
static addHook (forEvent, handler) {
const [cycle, event] = util.getCycleAndEvent(forEvent)
/**
* If user has defined wrong hook cycle, do let them know
*/
if (!this.$hooks[cycle]) {
throw CE.InvalidArgumentException.invalidParameter(`Invalid hook event {${forEvent}}`)
}
/**
* Add the handler
*/
this.$hooks[cycle].addHandler(event, handler)
}
/**
* Adds the global scope to the model global scopes.
*
* You can also give name to the scope, since named
* scopes can be removed when executing queries.
*
* @method addGlobalScope
*
* @param {Function} callback
* @param {String} [name = null]
*/
static addGlobalScope (callback, name = null) {
if (typeof (callback) !== 'function') {
throw CE
.InvalidArgumentException
.invalidParameter('Model.addGlobalScope expects a closure as first parameter')
}
this.$globalScopes.push({ callback, name })
return this
}
/**
* Attach a listener to be called everytime a query on
* the model is executed.
*
* @method onQuery
*
* @param {Function} callback
*
* @chainable
*/
static onQuery (callback) {
if (typeof (callback) !== 'function') {
throw CE.InvalidArgumentException.invalidParameter('Model.onQuery expects a closure as first parameter')
}
this.$queryListeners.push(callback)
return this
}
/**
* Adds a new trait to the model. Ideally it does a very
* simple thing and that is to pass the model class to
* your trait and you own it from there.
*
* @method addTrait
*
* @param {Function|String} trait - A plain function or reference to IoC container string
*/
static addTrait (trait) {
if (typeof (trait) !== 'function' && typeof (trait) !== 'string') {
throw CE
.InvalidArgumentException
.invalidParameter('Model.addTrait expects an IoC container binding or a closure')
}
/**
* If trait is a string, then point to register function
*/
trait = typeof (trait) === 'string' ? `${trait}.register` : trait
const { method } = resolver.forDir('modelTraits').resolveFunc(trait)
method(this)
}
/**
* Creates a new model instances from payload
* and also persist it to database at the
* same time.
*
* @method create
*
* @param {Object} payload
*
* @return {Model} Model instance is returned
*/
static async create (payload) {
const modelInstance = new this()
modelInstance.fill(payload)
await modelInstance.save()
return modelInstance
}
/**
* Creates many instances of model in parallel.
*
* @method createMany
*
* @param {Array} payloadArray
*
* @return {Array} Array of model instances is returned
*
* @throws {InvalidArgumentException} If payloadArray is not an array
*/
static async createMany (payloadArray) {
if (payloadArray instanceof Array === false) {
throw CE.InvalidArgumentException.invalidParameter(`${this.name}.createMany expects an array of values`)
}
return Promise.all(payloadArray.map((payload) => this.create(payload)))
}
/**
* Returns an object of values dirty after persisting to
* database or after fetching from database.
*
* @attribute dirty
*
* @return {Object}
*/
get dirty () {
return _.pickBy(this.$attributes, (value, key) => {
return _.isUndefined(this.$originalAttributes[key]) || this.$originalAttributes[key] !== value
})
}
/**
* Tells whether model is dirty or not
*
* @attribute isDirty
*
* @return {Boolean}
*/
get isDirty () {
return !!_.size(this.dirty)
}
/**
* Returns a boolean indicating if model is
* child of a parent model
*
* @attribute hasParent
*
* @return {Boolean}
*/
get hasParent () {
return !!this.$parent
}
/**
* Instantiate the model by defining constructor properties
* and also setting `__setters__` to tell the proxy that
* these values should be set directly on the constructor
* and not on the `attributes` object.
*
* @method instantiate
*
* @return {void}
*
* @private
*/
_instantiate () {
this.__setters__ = [
'$attributes',
'$persisted',
'primaryKeyValue',
'$originalAttributes',
'$relations',
'$sideLoaded',
'$parent',
'$frozen'
]
this.$attributes = {}
this.$persisted = false
this.$originalAttributes = {}
this.$relations = {}
this.$sideLoaded = {}
this.$parent = null
this.$frozen = false
}
/**
* Formats the date fields from the payload, only
* when they are marked as dates and there are
* no setters defined for them.
*
* Note: This method will mutate the existing object. If
* any part of your application doesn't want mutations
* then pass a cloned copy of object
*
* @method _formatDateFields
*
* @param {Object} values
*
* @return {Object}
*
* @private
*/
_formatDateFields (values) {
_(this.constructor.dates)
.filter((date) => {
return values[date] && typeof (this[util.getSetterName(date)]) !== 'function'
})
.each((date) => { values[date] = this.constructor.formatDates(date, values[date]) })
}
/**
* Checks for existence of setter on model and if exists
* returns the return value of setter, otherwise returns
* the default value.
*
* @method _getSetterValue
*
* @param {String} key
* @param {Mixed} value
*
* @return {Mixed}
*
* @private
*/
_getSetterValue (key, value) {
const setterName = util.getSetterName(key)
return typeof (this[setterName]) === 'function' ? this[setterName](value) : value
}
/**
* Checks for existence of getter on model and if exists
* returns the return value of getter, otherwise returns
* the default value
*
* @method _getGetterValue
*
* @param {String} key
* @param {Mixed} value
* @param {Mixed} [passAttrs = null]
*
* @return {Mixed}
*
* @private
*/
_getGetterValue (key, value, passAttrs = null) {
const getterName = util.getGetterName(key)
return typeof (this[getterName]) === 'function' ? this[getterName](passAttrs || value) : value
}
/**
* Sets `created_at` column on the values object.
*
* Note: This method will mutate the original object
* by adding a new key/value pair.
*
* @method _setCreatedAt
*
* @param {Object} values
*
* @private
*/
_setCreatedAt (values) {
const createdAtColumn = this.constructor.createdAtColumn
if (createdAtColumn) {
values[createdAtColumn] = this._getSetterValue(createdAtColumn, new Date())
}
}
/**
* Sets `updated_at` column on the values object.
*
* Note: This method will mutate the original object
* by adding a new key/value pair.
*
* @method _setUpdatedAt
*
* @param {Object} values
*
* @private
*/
_setUpdatedAt (values) {
const updatedAtColumn = this.constructor.updatedAtColumn
if (updatedAtColumn) {
values[updatedAtColumn] = this._getSetterValue(updatedAtColumn, new Date())
}
}
/**
* Sync the original attributes with actual attributes.
* This is done after `save`, `update` and `find`.
*
* After this `isDirty` should return `false`.
*
* @method _syncOriginals
*
* @return {void}
*
* @private
*/
_syncOriginals () {
this.$originalAttributes = _.clone(this.$attributes)
}
/**
* Insert values to the database. This method will
* call before and after hooks for `create` and
* `save` event.
*
* @method _insert
* @async
*
* @return {Boolean}
*
* @private
*/
async _insert () {
/**
* Executing before hooks
*/
await this.constructor.$hooks.before.exec('create', this)
/**
* Set timestamps
*/
this._setCreatedAt(this.$attributes)
this._setUpdatedAt(this.$attributes)
this._formatDateFields(this.$attributes)
const result = await this.constructor
.query()
.returning(this.constructor.primaryKey)
.insert(this.$attributes)
/**
* Only set the primary key value when incrementing is
* set to true on model
*/
if (this.constructor.incrementing) {
this.primaryKeyValue = result[0]
}
this.$persisted = true
/**
* Keep a clone copy of saved attributes, so that we can find
* a diff later when calling the update query.
*/
this._syncOriginals()
/**
* Executing after hooks
*/
await this.constructor.$hooks.after.exec('create', this)
return true
}
/**
* Update model by updating dirty attributes to the database.
*
* @method _update
* @async
*
* @return {Boolean}
*/
async _update () {
/**
* Executing before hooks
*/
await this.constructor.$hooks.before.exec('update', this)
let affected = 0
if (this.isDirty) {
/**
* Set proper timestamps
*/
affected = await this.constructor
.query()
.where(this.constructor.primaryKey, this.primaryKeyValue)
.ignoreScopes()
.update(this.dirty)
/**
* Sync originals to find a diff when updating for next time
*/
this._syncOriginals()
}
/**
* Executing after hooks
*/
await this.constructor.$hooks.after.exec('update', this)
return !!affected
}
/**
* Converts all date fields to moment objects, so
* that you can transform them into something
* else.
*
* @method _convertDatesToMomentInstances
*
* @return {void}
*
* @private
*/
_convertDatesToMomentInstances () {
this.constructor.dates.forEach((field) => {
if (this.$attributes[field]) {
this.$attributes[field] = moment(this.$attributes[field])
}
})
}
/**
* Set attribute on model instance. Setting properties
* manually or calling the `set` function has no
* difference.
*
* NOTE: this method will call the setter
*
* @method set
*
* @param {String} name
* @param {Mixed} value
*
* @return {void}
*/
set (name, value) {
this.$attributes[name] = this._getSetterValue(name, value)
}
/**
* Converts model to an object. This method will call getters,
* cast dates and will attach `computed` properties to the
* object.
*
* @method toObject
*
* @return {Object}
*/
toObject () {
let evaluatedAttrs = _.transform(this.$attributes, (result, value, key) => {
/**
* If value is an instance of moment and there is no getter defined
* for it, then cast it as a date.
*/
if (value instanceof moment && typeof (this[util.getGetterName(key)]) !== 'function') {
result[key] = this.constructor.castDates(key, value)
} else {
result[key] = this._getGetterValue(key, value)
}
return result
}, {})
/**
* Set computed properties when defined
*/
_.each(this.constructor.computed || [], (key) => {
evaluatedAttrs[key] = this._getGetterValue(key, null, evaluatedAttrs)
})
/**
* Pick visible fields or remove hidden fields
*/
if (_.isArray(this.constructor.visible)) {
evaluatedAttrs = _.pick(evaluatedAttrs, this.constructor.visible)
} else if (_.isArray(this.constructor.hidden)) {
evaluatedAttrs = _.omit(evaluatedAttrs, this.constructor.hidden)
}
return evaluatedAttrs
}
/**
* Persist model instance to the database. It will create
* a new row when model has not been persisted already,
* otherwise will update it.
*
* @method save
* @async
*
* @return {Boolean} Whether or not the model was persisted
*/
async save () {
return this.isNew ? this._insert() : this._update()
}
/**
* Deletes the model instance from the database. Also this
* method will freeze the model instance for updates.
*
* @method delete
* @async
*
* @return {Boolean}
*/
async delete () {
/**
* Executing before hooks
*/
await this.constructor.$hooks.before.exec('delete', this)
const affected = await this.constructor
.query()
.where(this.constructor.primaryKey, this.primaryKeyValue)
.ignoreScopes()
.delete()
/**
* If model was delete then freeze it modifications
*/
if (affected > 0) {
this.freeze()
}
/**
* Executing after hooks
*/
await this.constructor.$hooks.after.exec('delete', this)
return !!affected
}
/**
* Perform required actions to newUp the model instance. This
* method does not call setters since it is supposed to be
* called after `fetch` or `find`.
*
* @method newUp
*
* @param {Object} row
*
* @return {void}
*/
newUp (row) {
this.$persisted = true
this.$attributes = row
this._convertDatesToMomentInstances()
this._syncOriginals()
}
/**
* Find a row using the primary key
*
* @method find
* @async
*
* @param {String|Number} value
*
* @return {Model|Null}
*/
static find (value) {
return this.findBy(this.primaryKey, value)
}
/**
* Find a row using the primary key or
* fail with an exception
*
* @method findByOrFail
* @async
*
* @param {String|Number} value
*
* @return {Model}
*
* @throws {ModelNotFoundException} If unable to find row
*/
static findOrFail (value) {
return this.findByOrFail(this.primaryKey, value)
}
/**
* Find a model instance using key/value pair
*
* @method findBy
* @async
*
* @param {String} key
* @param {String|Number} value
*
* @return {Model|Null}
*/
static findBy (key, value) {
return this.query().where(key, value).first()
}
/**
* Find a model instance using key/value pair or
* fail with an exception
*
* @method findByOrFail
* @async
*
* @param {String} key
* @param {String|Number} value
*
* @return {Model}
*
* @throws {ModelNotFoundException} If unable to find row
*/
static findByOrFail (key, value) {
return this.query().where(key, value).firstOrFail()
}
/**
* Returns the first row. This method will add orderBy asc
* clause
*
* @method first
* @async
*
* @return {Model|Null}
*/
static first () {
return this.query().orderBy(this.primaryKey, 'asc').first()
}
/**
* Returns the first row or throw an exception.
* This method will add orderBy asc clause.
*
* @method first
* @async
*
* @return {Model}
*
* @throws {ModelNotFoundException} If unable to find row
*/
static firstOrFail () {
return this.query().orderBy(this.primaryKey, 'asc').firstOrFail()
}
/**
* Fetch everything from the database
*
* @method all
* @async
*
* @return {Collection}
*/
static all () {
return this.query().fetch()
}
/**
* Select x number of rows
*
* @method pick
* @async
*
* @param {Number} [limit = 1]
*
* @return {Collection}
*/
static pick (limit = 1) {
return this.query().pick(limit)
}
/**
* Select x number of rows in inverse
*
* @method pickInverse
* @async
*
* @param {Number} [limit = 1]
*
* @return {Collection}
*/
static pickInverse (limit = 1) {
return this.query().pickInverse(limit)
}
/**
* Returns an array of ids.
*
* Note: this method doesn't allow eagerloading relations
*
* @method ids
* @async
*
* @return {Array}
*/
static ids () {
return this.query().ids()
}
/**
* Returns an array of ids.
*
* Note: this method doesn't allow eagerloading relations
*
* @method ids
* @async
*
* @return {Array}
*/
static pair (lhs, rhs) {
return this.query().pair(lhs, rhs)
}
/**
* Sets a preloaded relationship on the model instance
*
* @method setRelated
*
* @param {String} key
* @param {Object|Array} value
*
* @throws {RuntimeException} If trying to set a relationship twice.
*/
setRelated (key, value) {
if (this.$relations[key]) {
throw CE.RuntimeException.overRidingRelation(key)
}
this.$relations[key] = value
/**
* Don't do anything when value doesn't exists
*/
if (!value) {
return
}
/**
* Set parent on model instance if value is instance
* of model.
*/
if (value instanceof Model) {
value.$parent = this.constructor.name
return
}
/**
* Otherwise loop over collection rows to set
* the $parent.
*/
_(value.rows)
.filter((val) => !!val)
.each((val) => (val.$parent = this.constructor.name))
}
/**
* Returns the relationship value
*
* @method getRelated
*
* @param {String} key
*
* @return {Object}
*/
getRelated (key) {
return this.$relations[key]
}
/**
* Loads relationships and set them as $relations
* attribute.
*
* To load multiple relations, call this method for
* multiple times
*
* @method load
* @async
*
* @param {String} relation
* @param {Function} callback
*
* @return {void}
*/
async load (relation, callback) {
const eagerLoad = new EagerLoad({ [relation]: callback })
const result = await eagerLoad.loadForOne(this)
_.each(result, (values, name) => this.setRelated(name, values))
}
/**
* Just like @ref('Model.load') but instead loads multiple relations for a
* single model instance.
*
* @method loadMany
* @async
*
* @param {Object} eagerLoadMap
*
* @return {void}
*/
async loadMany (eagerLoadMap) {
const eagerLoad = new EagerLoad(eagerLoadMap)
const result = await eagerLoad.loadForOne(this)
_.each(result, (values, name) => this.setRelated(name, values))
}
/**
* Returns an instance of @ref('HasOne') relation.
*
* @method hasOne
*
* @param {String|Class} relatedModel
* @param {String} primaryKey
* @param {String} foreignKey
*
* @return {HasOne}
*/
hasOne (relatedModel, primaryKey = this.constructor.primaryKey, foreignKey = this.constructor.foreignKey) {
return new HasOne(this, relatedModel, primaryKey, foreignKey)
}
/**
* Returns an instance of @ref('HasMany') relation
*
* @method hasMany
*
* @param {String|Class} relatedModel
* @param {String} primaryKey
* @param {String} foreignKey
*
* @return {HasMany}
*/
hasMany (relatedModel, primaryKey = this.constructor.primaryKey, foreignKey = this.constructor.foreignKey) {
return new HasMany(this, relatedModel, primaryKey, foreignKey)
}
/**
* Returns an instance of @ref('BelongsTo') relation
*
* @method belongsTo
*
* @param {String|Class} relatedModel
* @param {String} primaryKey
* @param {String} foreignKey
*
* @return {BelongsTo}
*/
belongsTo (relatedModel, primaryKey = relatedModel.foreignKey, foreignKey = relatedModel.primaryKey) {
return new BelongsTo(this, relatedModel, primaryKey, foreignKey)
}
/**
* Returns an instance of @ref('BelongsToMany') relation
*
* @method belongsToMany
*
* @param {Class|String} relatedModel
* @param {String} foreignKey
* @param {String} relatedForeignKey
* @param {String} primaryKey
* @param {String} relatedPrimaryKey
*
* @return {BelongsToMany}
*/
belongsToMany (
relatedModel,
foreignKey = this.constructor.foreignKey,
relatedForeignKey = relatedModel.foreignKey,
primaryKey = this.constructor.primaryKey,
relatedPrimaryKey = relatedModel.primaryKey
) {
return new BelongsToMany(this, relatedModel, primaryKey, foreignKey, relatedPrimaryKey, relatedForeignKey)
}
/**
* Returns instance of @ref('HasManyThrough')
*
* @method manyThrough
*
* @param {Class|String} relatedModel
* @param {String} relatedMethod
* @param {String} primaryKey
* @param {String} foreignKey
*
* @return {HasManyThrough}
*/
manyThrough (
relatedModel,
relatedMethod,
primaryKey = this.constructor.primaryKey,
foreignKey = this.constructor.foreignKey
) {
return new HasManyThrough(this, relatedModel, relatedMethod, primaryKey, foreignKey)
}
}
module.exports = Model