mongoose-fill
Version:
Mongoose.js add-on that adds virtual (temporary) async fileds API
451 lines (371 loc) • 11.9 kB
JavaScript
var mongoose = require('mongoose');
var async = require('async')
var util = require('util')
var getArgsWithOptions = function(__fill){
var args = [],
options = __fill.fill.options
var opts = __fill.opts && __fill.opts.length
? __fill.opts
: options
if (opts && opts.length){
// add lacking or remove excessive options
if (options){
var diff = opts.length - options.length
if (diff < 0){
opts = opts.concat(options.slice(diff))
} else if (diff > 0){
opts = opts.slice(0, diff)
}
}
args.push.apply(args, opts)
}
return args
}
var fillDoc = function(doc, __fill, cb){
var args = getArgsWithOptions(__fill)
if (__fill.fill.fill){
args.unshift(doc)
args.push(cb)
__fill.fill.fill.apply(__fill.fill, args)
} else if (__fill.fill.value) {
var props = __fill.props,
prop = props.length == 1 && props[0]
var multipleProps = __fill.fill.props.length > 1
//args.unshift(doc)
args.push(function(err, val){
// if val is not passed, just leave it
if (arguments.length > 1 && val !== doc){
if (prop){
doc.set(prop, multipleProps ? val[prop] : val)
} else {
props.forEach(function(prop){
doc.set(prop, val[prop])
})
}
}
cb(err, doc)
})
__fill.fill.value.apply(doc, args)
} else {
cb(null, doc)
}
}
var checkAlreadyHasFill = function(__fillsSequence, fill){
return __fillsSequence.reduce((has, __fills) =>
has || __fills
.filter(__fill => __fill.fill === fill)[0]
, false)
}
var checkAlreadyHasFillProp = function(__fillsSequence, prop){
return __fillsSequence.reduce((has, __fills) =>
has || __fills
.filter(__fill => __fill.fill.prop === prop).length > 0
, false)
}
var addFills = function(__fillsSequence, Model, props, opts, unshift) {
let __fills = []
props = (typeof props == 'string') ? props.split(' ') : props
props.forEach((prop) => {
prop = prop.trim()
var fill = Model.__fill && Model.__fill[prop]
if (fill){
var __fill = checkAlreadyHasFill(__fillsSequence, fill)
if (__fill){
if (__fill.props.indexOf(prop) < 0){
__fill.props.push(prop)
}
} else {
__fills.push({fill: fill, opts: opts, props: [prop]})
}
} else {
var propSplit = prop.split('.').filter(_ => _)
if (propSplit.length > 1){
if (checkAlreadyHasFillProp(__fillsSequence, prop)){
return
}
let propToFill = propSplit.slice(0, -1).join('.')
if (propToFill.indexOf('.') > 0 || (Model.__fill && Model.__fill[propToFill])) {
addFills(
__fillsSequence, Model,
[propToFill]
)
}
var fieldProp = propSplit.shift()
fill = {
prop: prop,
fill: function () {
var opts = Array.prototype.slice.call(arguments);
var doc = opts.shift()
var callback = opts.pop()
if (doc[fieldProp]){
var docs = doc[fieldProp]
if (!Array.isArray(docs)){
docs = [docs]
}
async.map(docs, (doc, cb) => {
var args = [propSplit.join('.')]
.concat(opts || []).concat([cb])
doc && typeof doc.fill === 'function'
? doc.fill.apply(doc, args)
: cb()
}, (err) => {
callback(err, doc)
})
} else {
callback(null, doc)
}
}
}
__fills.push({fill: fill, opts: opts, props: [prop]})
} else {
throw new Error('fill for property "' + prop + '" not found')
}
}
fill.db = Model.db
})
if (__fills.length){
unshift
? __fillsSequence.unshift(__fills)
: __fillsSequence.push(__fills)
}
}
var _exec = mongoose.Query.prototype.exec
var getPromise = function (cb) {
var promise, onResolve, resolve
if (global.Promise || mongoose.PromiseProvider) {
var Promise = global.Promise || mongoose.PromiseProvider.get().ES6
promise = new Promise((_resolve, _reject) => {
onResolve = () => {}
resolve = (err, res) => {
err ? _reject(err) : _resolve(res)
cb && cb(err, res)
}
})
} else {
promise = new mongoose.Promise();
onResolve = promise.onResolve.bind(promise)
resolve = promise.resolve.bind(promise)
}
return {
promise: promise,
onResolve: onResolve,
resolve: resolve
}
}
mongoose.Query.prototype.exec = function (op, cb) {
var __fillsSequence = this.__fillsSequence || []
var query = this
if (query.model.__fill && this._fields){
Object.keys(this._fields).forEach(function(f){
if (query._fields[f] == 1 && query.model.__fill[f]){
addFills(__fillsSequence, query.model, f, true)
}
})
}
if (!__fillsSequence.length) {
return _exec.apply(this, arguments);
}
if (typeof op === 'function') {
cb = op;
op = null;
}
var p = getPromise(cb)
var promise = p.promise, onResolve = p.onResolve, resolve = p.resolve
_exec.call(this, op, function (err, docs) {
if (err || !docs) {
resolve(err, docs)
} else {
async.mapSeries(__fillsSequence, function(__fills, cb){
async.map(__fills, function(__fill, cb){
var useMultiWithSingle = !util.isArray(docs) && !__fill.fill.value && __fill.fill.multi
if (useMultiWithSingle){
docs = [docs]
}
// TODO: make this also if there is only multi methods when one doc
if (util.isArray(docs)){
var args = getArgsWithOptions(__fill)
if (__fill.fill.multi && !__fill.fillEach){
var index = {},
ids = docs.map(function(doc){
var id = doc._id.toString()
index[id] = doc
return doc._id
}, {})
args.unshift(docs, ids)
var multipleProps = __fill.fill.props.length > 1
// set default values
if (__fill.fill.default){
__fill.fill.props.forEach(function(prop, i){
var defaultPropValue =
multipleProps && Array.isArray(__fill.fill.default)
? __fill.fill.default[i]
: __fill.fill.default
docs.forEach(function(doc){
// ensure unique values of passed objects/arrays
if (typeof defaultPropValue == 'object'){
doc[prop] = Object.assign(
Array.isArray(defaultPropValue) ? [] : {}
, defaultPropValue)
} else if (typeof defaultPropValue == 'function'){
doc[prop] = defaultPropValue()
} else {
doc[prop] = defaultPropValue
}
})
})
}
args.push(function(err, results){
if (results && results !== docs){
// convert object map to array in right order
if (!util.isArray(results)){
results = ids.map(function(id){
return results[id]
})
}
results.forEach(function(r, i){
if (!r){
return
}
var doc = docs[i]
var spreadProps = multipleProps
// this is not the best idea, but we will allow this
if (r._id && index[r._id.toString()]){
spreadProps = true
doc = index[r._id.toString()]
}
if (!doc){return}
if (spreadProps){
__fill.props.forEach(function(prop){
doc[prop] = r[prop]
})
} else {
var prop = __fill.fill.props[0]
doc[prop] = r
}
})
}
if (useMultiWithSingle){
cb(err, docs[0])
} else {
cb(err, docs)
}
})
__fill.fill.multi.apply(__fill.fill, args)
} else {
async.map(docs, function(doc, cb){
fillDoc(doc, __fill, cb)
}, cb)
}
} else {
fillDoc(docs, __fill, cb)
}
}, function(err){
cb(err)
})
}, function(err){
resolve(err, docs)
})
}
});
return promise
}
mongoose.Schema.prototype.fill = function(props, def) {
this.statics.__fill = this.statics.__fill || {}
// return
if (this.statics.__fill[props]){
return this.statics.__fill[props]
}
def = def || {}
if (typeof def == 'function') {
def = {value: def}
}
var self = this
props = props.split(' ')
def.props = props
props.forEach(function (prop) {
self.statics.__fill[prop] = def
self.virtual(prop).get(function () {
return this['__' + prop]
}).set(function (val) {
this['__' + prop] = val
})
})
var defFiller = [
'value', 'multi',
'query', 'debug', 'default', 'options']
.reduce(function(defFiller, method){
if (method == 'options'){
defFiller[method] = function(){
def[method] = Array.prototype.slice.call(arguments);
return defFiller
}
} else {
defFiller[method] = function(val){
def[method] = val
return defFiller
}
}
return defFiller
}, {})
defFiller.get = defFiller.value
return defFiller
}
mongoose.Query.prototype.fill = function() {
var query = this;
var Model = this.model;
var args = Array.prototype.slice.call(arguments);
var props = args.shift()
var opts = args
query.__fillsSequence = query.__fillsSequence || []
query.__fills = query.__fills || []
addFills(query.__fillsSequence, Model, props, opts)
return this
}
// force single fill
mongoose.Query.prototype.fillEach = function() {
var prevLen = this.__fills ? query.__fills.length : 0
this.fill.apply(this, arguments)
for (var i = this.__fills.length - 1; i >= prevLen; i-- ){
this.__fills[i].fillEach = true
}
return this
}
function prototypeFill (doc, Model) {
return function () {
var args = Array.prototype.slice.call(arguments);
var props = args.shift()
var cb = typeof args[args.length - 1] == 'function' && args.pop()
var opts = args
var Promise = global.Promise || mongoose.PromiseProvider.get().ES6
var __fillsSequence = []
addFills(__fillsSequence, Model, props, opts)
let p = getPromise(cb)
async.mapSeries(__fillsSequence, (__fills, cb) => {
async.map(__fills, function(__fill, cb){
fillDoc(doc, __fill, cb)
}, cb)
}, (err) => {
p.resolve(err, doc)
})
return p.promise
}
}
mongoose.Model.prototype.fill = function(){
return prototypeFill(this, this.constructor).apply(this, arguments)
}
mongoose.Types.Embedded.prototype.fill = function(){
return prototypeFill(this, this.schema.statics).apply(this, arguments)
}
mongoose.Types.Embedded.prototype.filled =
mongoose.Model.prototype.filled = function(){
var args = Array.prototype.slice.call(arguments);
var cb = args[args.length - 1]
if (typeof cb == 'function'){
args[args.length - 1] = function(err, doc){
cb(err, doc[prop])
}
}
this.fill.apply(this, args)
}
module.exports = mongoose