lib/GenericModel.js
'use strict'
const ObjectUtils = require('@contartec-team/object-utils')
const validate = require('validate.js')
const QueryBuilderUtils = require('./utils/QueryBuilderUtils')
const bookshelf = global.bookshelfInstance
const knex = global.bookshelfInstance.knex
const DEFAULT_ATTRIBUTES = {
requireFetch: false
}
/**
* The `GenericModel` params
*
* @typedef GenericModelParams
* @type {Object}
* @memberof GenericModel
*
* @description A mix of `bookshelf` and custom options (override this with the desired one)
*
* @property {(string | Array<string>)} idAttribute The id attr name(s)
* @property {Boolean} hasTimestamps Whether it has `created_at, updated_at, deleted_at, restored_at` or not
*/
const DEFAULT_GENERIC_MODEL_PARAMS = {
idAttribute : 'id',
hasTimestamps : true
}
/**
* Generic model for `bookshelf` ORM
* @class GenericModel
* @extends {bookshelf.Model}
*/
class GenericModel extends bookshelf.Model {
/**
* The `GenericModel` params
* @description A mix of `bookshelf` and custom options (override this with the desired one)
*
* @type {GenericModelParams}
*/
static get GENERIC_MODEL_PARAMS() { return {} }
/**
* The `GenericModel` params used internally
*
* @type {GenericModelParams}
*/
static get MODEL_PARAMS() {
return {
...this.DEFAULT_GENERIC_MODEL_PARAMS,
...this.GENERIC_MODEL_PARAMS
}
}
/**
* The default `GenericModel` params
*
* @type {GenericModelParams}
*
* @property {string} [idAttribute = 'id']
* @property {Boolean} [hasTimestamps = true]
*/
static get DEFAULT_GENERIC_MODEL_PARAMS() { return DEFAULT_GENERIC_MODEL_PARAMS }
/**
* The default filter params for [`.getAll`]{@link GenericModel.getAll}
* @override
* @type {Object}
*/
static get GET_ALL_DEFAULT_PARAMS() {
return {
page : 1,
pageSize : 500
}
}
/**
* The default `fetch` for [`.getAll`]{@link GenericModel.getAll}
* @override
* @type {Object}
*/
static get GET_ALL_DEFAULT_FETCH_OPTIONS() {
return {
withRelated: this.relateds
}
}
/**
* The default filter params for [`.getCount`]{@link GenericModel.getCount}
* @override
* @type {Object}
*/
static get COUNT_DEFAULT_PARAMS() {
return {}
}
static get bookshelf() { return bookshelf }
static get knex() { return bookshelf.knex }
get tableName() { return this._tableName }
get idAttribute() { return this._idAttribute || 'id' }
get hasTimestamps() { return this._hasTimestamps }
get soft() { return this._soft }
get requireFetch() { return this._requireFetch }
get visible() { return this._visible || [] }
get hidden() { return this._hidden || [] }
get constraints() { return this._constraints }
get virtuals() { return this._virtuals }
get relateds() { return this._relateds }
get _SERIALIZE_OPTIONS() {
return {
shallow : false,
omitPivot : false,
virtuals : true
}
}
/**
* @deprecated Use `static get GENERIC_MODEL_PARAMS()` instead
* @constructor
* @param {GenericModelParams} properties The `model` properties
* @param {Object} object The `model` object
*/
constructor (properties, object) {
super()
const propertiesTemp = { ...DEFAULT_ATTRIBUTES, ...properties }
Object
.assign(this, ObjectUtils.createPrivateAttributes(propertiesTemp))
this
._setValues(object)
._setId(object)
._setRelateds(object)
if (this._hasAttributes() || propertiesTemp.virtuals)
this._setVirtuals(propertiesTemp.virtuals, object)
}
getIdObject() {
return !this._isCompositePrimaryKey() ?
{ [this.idAttribute]: this.id } :
this.id
}
getAttributes() {
let attributes = []
if (this._hasVisibleAttributes()) {
attributes = this.visible
if (this.hidden)
attributes = attributes.concat(this.hidden)
}
return attributes
}
getQueryAll(params, queryBuilder, isWhere) {
return GenericModel
.getQueryAll(params, this, queryBuilder, isWhere)
}
delete() {
const whereParams = this.getIdObject()
let methodName = 'del'
let methodParams = null
this.trigger('deleting')
if (this.soft) {
methodName = 'update'
methodParams = {
...this.toJSON({ virtuals: false, shallow: true }),
deleted_at : new Date(),
updated_at : new Date(),
restored_at : null
}
}
return new Promise((resolve, reject) => {
return this
.query()
.where(whereParams)
[methodName](methodParams)
.then(result => {
this.trigger('deleted')
resolve(result)
})
.catch(reject)
})
}
setVirtual(name, virtual) {
let getter, setter
if (virtual && virtual.get) {
getter = virtual.get
setter = virtual.set ?
virtual.set :
function(value) { virtual = value }
}
else
getter = virtual
if (typeof(getter) != 'function')
getter = function() { return virtual }
this._virtuals[name] = {
get : getter,
set : setter
}
return this
}
async isPersisted() {
let count = 0
if (this.id) {
const params = this.getIdObject()
count = await this
.where(params)
.count(params)
}
return count == 1
}
isDeleted() {
return this.get('deleted_at') > (this.get('restored_at') || null)
}
async areConstraintsValids() {
const idParams = this.getIdObject()
const constraintParams = this.
_createObjectByArrayAttributes(this.constraints)
const count = await this
.query(qb => {
qb
.where(constraintParams)
for (const attr in idParams)
qb.andWhere(attr, '<>', idParams[attr] || 0)
})
.count()
return count == 0
}
isObjectValid() {
return this.validation() == null
}
validation(format) {
return validate(this.toJSON({ virtuals: false, shallow: true }), this._validationRules, format)
}
serialize(options) {
const optionsTemp = { ...this._SERIALIZE_OPTIONS, ...options }
let json = super
.serialize(optionsTemp)
if (optionsTemp.virtuals)
json = { ...this._getVirtuals(), ...json }
if (!optionsTemp.shallow && this.relateds) {
this.relateds
.forEach(related => {
json[related] = this.related(related).toJSON()
})
}
if (this.hidden) {
for (let i = this.hidden.length - 1; i >= 0; i--)
delete json[this.hidden[i]]
}
if (!optionsTemp.omitPivot) {
const pivots = this._getPivots(this.attributes)
for (const pivotName in pivots) {
const attrName = pivotName.replace('_pivot_', '')
json[attrName] = pivots[pivotName]
}
}
return json
}
toJSON(options) {
return this
.serialize(options)
}
_getId(object) {
if (!object)
object = this.getAttributes()
let id = {}
if (object && this._hasIdAttribute()) {
if (this._isCompositePrimaryKey()) {
this.idAttribute
.forEach(
attr =>
id[attr] = object[attr]
)
}
else
id = object[this.idAttribute]
}
return id
}
_getAttributes(object) {
let visibleAttributes = {}
if (this._hasVisibleAttributes()) {
const definedAttributes = this.getAttributes()
for (const attr in object) {
if (definedAttributes.indexOf(attr) >= 0)
visibleAttributes[attr] = object[attr]
}
}
else
visibleAttributes = object
return visibleAttributes
}
_getVisibleAttributes(object) {
let visibleAttributes = {}
if (this._hasVisibleAttributes()) {
for (const attr in object) {
if (this._hasVisibleAttribute(attr))
visibleAttributes[attr] = object[attr]
}
}
else
visibleAttributes = object
return visibleAttributes
}
_getHiddenAttributes(object) {
let hiddenAttributes = {}
if (this._hasHiddenAttributes()) {
for (const attr in object) {
if (this._hasHiddenAttribute(attr))
hiddenAttributes[attr] = object[attr]
}
}
else
hiddenAttributes = object
return hiddenAttributes
}
_getNotDefinedAttributes(object) {
let notDefinedAttributes = {}
if (this._hasAttributes()) {
for (const attr in object) {
if (!this._hasAttribute(attr) && !attr.startsWith('_pivot_'))
notDefinedAttributes[attr] = object[attr]
}
}
else
notDefinedAttributes = object
return notDefinedAttributes
}
_getRelateds(object) {
const relateds = {}
if (this._hasRelateds()) {
for (const attr in object) {
if (this._hasRelated(attr))
relateds[attr] = object[attr]
}
}
return relateds
}
_getPivots(object) {
let pivots = {}
if (this.pivot)
pivots = { ...this.pivot.attributes }
for (const attr in object) {
if (attr.startsWith('_pivot_'))
pivots[attr] = object[attr]
}
return pivots
}
_getVirtuals() {
const virtuals = {}
const virtualNames = {
...this._virtuals,
...this._getNotDefinedAttributes(this.attributes)
}
if (virtualNames != {}) {
for (const virtualName in virtualNames)
virtuals[virtualName] = this._getVirtual(virtualName)
}
return virtuals
}
_getVirtual(name) {
let virtual = null
if (this.attributes[name])
virtual = this.attributes[name]
else if (this._virtuals[name]) {
virtual = this._virtuals[name].get ?
this._virtuals[name].get.call(this) :
this._virtuals[name].call(this)
}
return virtual
}
_createObjectByArrayAttributes(arrayAttributes) {
const params = {}
arrayAttributes
.forEach(
attr =>
params[attr] = this.get(attr)
)
return params
}
_isCompositePrimaryKey() {
return this.idAttribute.constructor.name == 'Array'
}
_hasIdAttribute() {
return this.idAttribute != null
}
_hasVisibleAttributes() {
return this.visible && this.visible.length
}
_hasHiddenAttributes() {
return this.hidden && this.hidden.length
}
_hasRelateds() {
return this.relateds && this.relateds.length
}
_hasAttributes() {
return this._hasVisibleAttributes() || this._hasHiddenAttributes()
}
_hasVisibleAttribute(attr) {
let hasAttr = false
if (this._hasVisibleAttributes())
hasAttr = this.visible.indexOf(attr) >= 0
return hasAttr
}
_hasHiddenAttribute(attr) {
let hasAttr = false
if (this._hasHiddenAttributes())
hasAttr = this.hidden.indexOf(attr) >= 0
return hasAttr
}
_hasAttribute(attr) {
return this._hasVisibleAttribute(attr) || this._hasHiddenAttribute(attr)
}
_hasRelated(attr) {
let hasAttr = false
if (this._hasRelateds())
hasAttr = this.relateds.indexOf(attr) >= 0
return hasAttr
}
_setValues(object) {
Object
.assign(this.attributes, this._getAttributes(object))
Object
.assign(this.attributes, this._getPivots(object))
return this
}
_setId(object) {
this.id = this._getId(object)
return this
}
_setRelateds(object) {
if (this._hasRelateds()) {
const relatedObjects = this._getRelateds(object)
for (var i = this.relateds.length - 1; i >= 0; i--) {
const relatedName = this.relateds[i]
this
.related(relatedName)
.set(relatedObjects[relatedName])
}
}
return this
}
_setVirtuals(virtuals = {}, object = {}) {
const attrsNotDefined = this._getNotDefinedAttributes(object)
this._virtuals = { ...virtuals }
if (object && this._hasVisibleAttributes()) {
for (const virtualName in attrsNotDefined) {
this
.setVirtual(virtualName, attrsNotDefined[virtualName])
}
}
return this
}
/**
* Returns the list of objects
*
* @param {Object} params The `params` object to filter the attrs
* @param {Number} params.page The `page` itens
* @param {Number} params.pageSize The `page` size
* @param {(Object | string)} params.orderBy The `attrName` to order for (asc) or an object, `attrName: 'ASC | DESC'`
* @param {Array} params.distinct The `distinct` attrs to group
* @param {string} params.genericSearch The `genericSearch` text
* @param {Object} fetchOptions The `bookshelf` fetch options (@see {@link https://bookshelfjs.org/api.html#Model-instance-fetch})
*
* @return {Promise<Array>} The resulted list
*/
static getAll(params, fetchOptions = {}) {
const paramsTemp = { ...this.GET_ALL_DEFAULT_PARAMS, ...params }
const fetchOptionsTemp = { ...this.GET_ALL_DEFAULT_FETCH_OPTIONS, ...fetchOptions }
if (typeof(paramsTemp.orderBy) == 'string')
paramsTemp.orderBy = { [paramsTemp.orderBy]: 'ASC' }
if (typeof(paramsTemp.groupBy) == 'string')
paramsTemp.groupBy = [paramsTemp.groupBy]
if (paramsTemp.relateds) {
fetchOptionsTemp.withRelated = paramsTemp.relateds instanceof Array ?
[ ...paramsTemp.relateds ] :
[ paramsTemp.relateds ]
}
return this
.query(qb => {
qb = this
._getQueryAll(paramsTemp, qb)
.offset((paramsTemp.page - 1) * paramsTemp.pageSize)
.limit(paramsTemp.pageSize)
if (paramsTemp.distinct) {
qb
.select(knex.raw(`DISTINCT ON (${paramsTemp.distinct.join(',')}) *`))
.orderBy(paramsTemp.distinct)
}
for (const attr in paramsTemp.orderBy)
qb.orderBy(attr, paramsTemp.orderBy[attr] || 'ASC')
if (paramsTemp.groupBy && paramsTemp.groupBy instanceof Array) {
paramsTemp.groupBy
.forEach(attr => qb.groupBy(attr))
}
})
.fetchAll(fetchOptionsTemp)
}
/**
* Returns the count of objects
* @async
*
* @param {Object} params The `params` object to filter the attrs
*
* @return {Number} The count value
*/
static async getCount(params) {
const paramsTemp = { ...this.COUNT_DEFAULT_PARAMS, ...params }
const queryBuilder = this
.query(qb => {
qb = this
._getQueryAll(paramsTemp, qb)
if (paramsTemp.countDistinct)
qb.countDistinct(paramsTemp.countDistinct)
})
let count = 0
if (paramsTemp.countDistinct) {
const result = await queryBuilder.fetch()
count = parseInt(result.get('count'))
}
else
count = await queryBuilder.count()
return parseInt(count)
}
/**
* Return one object
* @async
*
* @param {Object} params The `params` object to filter the attrs
*
* @param {Object} fetchOptions The `bookshelf` fetch options (@see {@link https://bookshelfjs.org/api.html#Model-instance-fetch})
*
* @return {Object} The object found
*/
static async getOne(params = {}, fetchOptions = {}) {
const paramsTemp = {
...params,
page : 1,
pageSize : 1
}
const models = await this
.getAll(paramsTemp, fetchOptions)
return models.at(0)
}
/**
* Returns the model by id
*
* @param {*} id The id attr
* @param {Object} [options] The `bookshelf` fetch options
*
* @return {Promise<Object>} The model
*/
static findById(id, options = {}) {
const fetchParams = typeof(id) == 'object' ?
{ ...id } :
{ [this.MODEL_PARAMS.idAttribute]: id }
const fetchOptions = {
...options,
require: false
}
if (this.MODEL_PARAMS.soft)
fetchParams.deleted_at = null
return this
.where(fetchParams)
.fetch(fetchOptions)
}
static getAttributes(model) {
let attributes = []
if (model.visible && model.visible.length) {
attributes = model.visible
if (model.hidden && model.hidden.length)
attributes = attributes.concat(model.hidden)
}
return attributes
}
static getQueryAll(params, model, queryBuilder, isWhere = true) {
const queryBoolean = isWhere ?
'where' :
'orWhere'
const attributes = GenericModel.getAttributes(model)
if (!queryBuilder) {
model.resetQuery()
queryBuilder = Object
.assign({}, model.query())
.and
}
for (const param in params) {
const whereObject = QueryBuilderUtils
.getWhereObject(param, attributes)
if (!attributes.length || attributes.indexOf(whereObject.attr) >= 0) {
const attrName = `${model.tableName}.${whereObject.attr}`
if (params[param] != null) {
if ((whereObject.operator == 'NOT IN' || whereObject.operator == 'IN') && params[param] && !Array.isArray(params[param]))
params[param] = [params[param]]
queryBuilder
[`${queryBoolean}`](attrName, whereObject.operator, params[param])
}
else {
queryBuilder
[`${queryBoolean}Null`](attrName, params[param])
}
}
else if (model._hasRelateds() && model.relateds.indexOf(whereObject.attr) >= 0 && typeof(params[param]) == 'object') {
const related = GenericModel
.getRelatedParams(whereObject.attr, model)
const relatedQuery = GenericModel
.getQueryAll(params[param], related.model, null, isWhere)
.toString()
.split('where')[1]
queryBuilder
.leftJoin(related.tableName, `${related.tableName}.${related.idAttribute}`, `${model.tableName}.${related.attrName}`)
[`${queryBoolean}Raw`](relatedQuery)
if (GenericModel._isRelatedManyToMany(whereObject.attr, model)) {
const relatedManyToMany = GenericModel
.getRelatedManyToManyParams(whereObject.attr, model)
queryBuilder
.leftJoin(relatedManyToMany.tableName, `${relatedManyToMany.tableName}.${relatedManyToMany.idAttribute}`, `${related.tableName}.${relatedManyToMany.attrName}`)
}
}
}
model.resetQuery()
return queryBuilder
}
static getRelatedParams(relatedName, model) {
return {
tableName : GenericModel._getRelatedTableName(relatedName, model),
attrName : GenericModel._getRelatedAttrName(relatedName, model),
idAttribute : GenericModel._getRelatedIdAttribute(relatedName, model),
model : GenericModel._getRelatedModel(relatedName, model)
}
}
static getRelatedManyToManyParams(relatedName, model) {
return {
tableName : GenericModel._getRelatedManyToManyTable(relatedName, model),
attrName : GenericModel._getRelatedManyToManyAttrName(relatedName, model),
idAttribute : GenericModel._getRelatedManyToManyIdAttribute(relatedName, model)
}
}
/**
* Returns whether it's persisted or not
* @async
*
* @param {(string | Number | Object)} id The `id` value
*
* @return {Boolean} Whether it's persisted or not
*/
static async isPersisted(id) {
const params = id instanceof Object ?
{ ...id } :
{ [this.MODEL_PARAMS.idAttribute]: id }
const count = await this
.where(params)
.count()
return count >= 1
}
static _getRelatedData(relatedName, model) {
return model
.related(relatedName)
.relatedData
}
static _getRelatedModel(relatedName, model) {
const relatedData = GenericModel
._getRelatedData(relatedName, model)
return new relatedData
.target()
}
static _getRelatedIdAttribute(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
let idAttribute = relatedModelData.foreignKeyTarget
if (!idAttribute) {
if (relatedModelData.foreignKey)
idAttribute = relatedModelData.targetIdAttribute
else
idAttribute = relatedModelData.key('foreignKey')
}
return idAttribute
}
static _getRelatedAttrName(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
return relatedModelData.foreignKey || relatedModelData.parentIdAttribute
}
static _getRelatedTableName(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
return GenericModel.
_isRelatedManyToMany(relatedName, model) ?
relatedModelData.joinTable() :
relatedModelData.targetTableName
}
static _getRelatedManyToManyIdAttribute(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
return relatedModelData.targetIdAttribute
}
static _getRelatedManyToManyAttrName(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
return relatedModelData.key('otherKey')
}
/**
* Returns the default `queryBuilder` query
* @override
*
* @param {Object} params The `params` object
* @param {string} params.genericSearch The `generic search` text
* @param {Object} queryBuilder The `knex` `queryBuilder` object
*
* @return {Object} The `knex` `queryBuilder` object
*/
static _getQueryAll(params, queryBuilder) {
const modelInstance = Reflect.construct(this, [{}])
queryBuilder = this
.getQueryAll(params, modelInstance, queryBuilder)
if (params.genericSearch) {
queryBuilder = this
._getQueryGenericSearch(params.genericSearch, queryBuilder)
}
return queryBuilder
}
/**
* Returns the `queryBuilder` query for `generic search`
* @override
*
* @param {string} genericSearch The `generic search` text
* @param {Object} queryBuilder The `knex` `queryBuilder` object
*
* @return {Object} The `knex` `queryBuilder` object
*/
static _getQueryGenericSearch(genericSearch, queryBuilder) {
return queryBuilder
}
static _getRelatedManyToManyTable(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
return relatedModelData.targetTableName
}
static _isRelatedManyToMany(relatedName, model) {
const relatedModelData = GenericModel
._getRelatedData(relatedName, model)
return relatedModelData.isJoined()
}
}
module.exports = GenericModel