vitamin
Version:
Data Mapper library for Node.js applications
357 lines (296 loc) • 8.32 kB
JavaScript
import _ from 'underscore'
import Model from '../model'
import Relation from './base'
import Promise from 'bluebird'
import Mapper from '../mapper'
import mixin from './mixins/one-to-many'
// exports
export default class extends mixin(Relation) {
/**
* BelongsToManyRelation constructor
*
* @param {Mapper} parent mapper instance
* @param {Mapper} target mapper instance
* @param {String} pivot table name
* @param {String} pfk parent model foreign key
* @param {String} tfk target model foreign key
* @constructor
*/
constructor(parent, target, pivot, pfk, tfk) {
super(parent, target)
this.pivot = new Mapper({ tableName: pivot })
this.table = this.pivot.tableName
this.localKey = parent.primaryKey
this.pivotColumns = [pfk, tfk]
this.targetKey = tfk
this.otherKey = pfk
this._through = this.newPivotQuery(false).from(this.table, target.name + '_pivot')
}
/**
* Add the pivot table columns to fetch
*
* @param {Array} columns
* @return this relation
*/
withPivot(columns) {
if (! _.isArray(columns) ) columns = _.toArray(arguments)
this.pivotColumns = _.uniq(this.pivotColumns.concat(columns))
return this
}
/**
* Update an existing record on the pivot table
*
* @param {Any} id of the target model
* @param {Object} attrs
* @return promise
*/
updatePivot(id, attrs) {
return this.newPivotQuery().where(this.targetKey, id).update(attrs)
}
/**
* Create and attach a new instance of the related model
*
* @param {Object} attrs
* @param {Array} returning
* @return promise
*/
create(attrs, returning = ['*']) {
var pivots = {}
if ( attrs.pivot ) {
pivots = attrs.pivot
delete attrs.pivot
}
return this.save(this.target.newInstance(attrs), pivots, returning)
}
/**
* Save a new model and attach it to the parent
*
* @param {Model} related
* @param {Object} pivots
* @param {Array} returning
* @return promise
*/
save(related, pivots = {}, returning = ['*']) {
return super.save(related, returning).then(model => {
return this.attach(this.createPivotRecord(model.getId(), pivots))
})
}
/**
* Save many models and attach them to the parent model
*
* @param {Array} related
* @param {Array} pivots
* @parma {Array} returning
* @return promise
*/
saveMany(related, pivots = [], returning = ['*']) {
return Promise.map(related, (model, i) => {
return this.save(model, pivots[i], returning)
})
}
/**
* Attach a models to the parent model
*
* @param {Array} ids
* @return promise
*/
attach(ids) {
var records = this.createPivotRecords(_.isArray(ids) ? ids : [ids])
return this.newPivotQuery(false).insert(records)
}
/**
* Detach one or many models from the parent
*
* @param {Array} ids
* @return promise
*/
detach(ids = []) {
var query = this.newPivotQuery()
ids = (_.isArray(ids) ? ids : [ids]).map(value => {
return (value instanceof Model) ? value.getId() : value
})
if ( ids.length > 0 ) query.whereIn(this.targetKey, ids)
return query.destroy()
}
/**
* Toggle the given IDs in the intermediate table
*
* It will detach the existing given ids, and attach the new ones,
* In another words, it will only sync the given ids
*
* @param {Any} ids
* @return promise
*/
toggle(ids) {
var toDetach = [], toAttach = []
if (! _.isArray(ids) ) ids = [ids]
return this
.newPivotQuery()
.pluck(this.targetKey)
// traversing
.then(oldIds => {
ids.forEach(_id => {
if ( _id instanceof Model ) _id = _id.getId()
if ( _.isArray(_id) ) _id = _id[0]
_.contains(oldIds, _id) ? toDetach.push(_id) : toAttach.push(_id)
})
})
// detach
.then(() => _.isEmpty(toDetach) ? false : this.detach(toDetach))
// attach
.then(() => _.isEmpty(toAttach) ? false : this.attach(toAttach))
// return
.then(() => {
return {
'detached': toDetach.length,
'attached': toAttach.length,
}
})
}
/**
* Sync the intermediate table with a list of IDs
*
* @param {Array} ids
* @return promise
*/
sync(ids) {
var newIds = [], toAttach = [], toUpdate = [], toDetach
return this
.newPivotQuery()
.pluck(this.targetKey)
// traversing
.then(oldIds => {
ids.forEach(value => {
var id
if ( value instanceof Model ) value = value.getId()
if (! _.isArray(value) ) value = [value]
// add the new id
newIds.push(id = value[0])
// looking for ids that should be attached or updated
if (! _.contains(oldIds, id) ) toAttach.push(value)
else if (! _.isEmpty(value[1]) ) toUpdate.push(value)
})
toDetach = _.difference(oldIds, newIds)
})
// detach
.then(() => _.isEmpty(toDetach) ? false : this.detach(toDetach))
// attach
.then(() => _.isEmpty(toAttach) ? false : this.attach(toAttach))
// update pivots
.then(() => Promise.map(toUpdate, args => this.updatePivot(...args)))
// return
.then(() => {
return {
'detached': toDetach.length,
'attached': toAttach.length,
'updated': toUpdate.length,
}
})
}
/**
* Apply constraints on the relation query
*
* @param {Model} model
* @return this relation
*/
addConstraints(model) {
this.addPivotJoin()
this.addPivotColumns()
return super.addConstraints(model)
}
/**
* Create a query for the pivot table
*
* @param {Boolean} constraints
* @return query
* @private
*/
newPivotQuery(constraints = true) {
var query = this.pivot.newQuery()
if ( constraints )
query.where(this.otherKey, this.model.get(this.localKey))
return query
}
/**
* Create an array of records to insert into the pivot table
*
* @param {Array} ids
* @return array
* @private
*/
createPivotRecords(ids) {
return ids.map(args => {
if ( args instanceof Model ) args = [args.getId()]
if (! _.isArray(args) ) args = [args]
return this.createPivotRecord(...args)
})
}
/**
* Create a record to insert into the pivot table
*
* @param {Any} id of the target model
* @param {Object} pivots pivot table attributes
* @return plain object
* @private
*/
createPivotRecord(id, pivots = {}) {
var record = _.extend({}, pivots)
record[this.otherKey] = this.model.get(this.localKey)
record[this.targetKey] = id
return record
}
/**
* Get the fully qualified compare key of the relation
*
* @return string
* @private
*/
getCompareKey() {
return this._through.getQualifiedColumn(this.otherKey)
}
/**
* Apply eager constraints on the relation query
*
* @param {Array} models
* @private
*/
addEagerLoadConstraints(models) {
super.addEagerLoadConstraints(models)
this.addPivotColumns()
this.addPivotJoin()
}
/**
* Set the columns of the relation query
*
* @private
*/
addPivotColumns() {
var columns = this.pivotColumns.map(col => {
return this._through.getQualifiedColumn(col) + ' as pivot_' + col
})
this.query.builder.select(columns)
}
/**
* Build model dictionary keyed by the given key
*
* @param {Collection} related
* @param {String} key
* @return plain object
* @private
*/
buildDictionary(related, key) {
return related.groupBy(model => model.related.pivot.get(key))
}
/**
* Add `join`constraints to the intermediate table
*
* @private
*/
addPivotJoin() {
this.query.join(
this._through.table + ' as ' + this._through.alias,
this._through.getQualifiedColumn(this.targetKey),
this.query.getQualifiedColumn(this.target.primaryKey)
)
}
}