Loopback lacks out of the box support for computed properties that are dependent on related models, because related models are loaded asynchronously. However, I wrote a solution to address this problem (pardon the coffeescript):
app.wrapper = (model, fn, args)->
deferred = Q.defer()
args.push((err, result)->
console.log(err) if err
throw err if err
deferred.resolve(result)
)
app.models[model][fn].apply(app.models[model], args)
return deferred.promise
app.mixCalcs = (model, fn, args)->
mainDeferred = Q.defer()
iterationDeferreds = new Array()
mixinCalcs = (model, relationHash) ->
#iterate if there if the model includes relations
if relationHash.scope? and relationHash.scope.include?
#test if hash includes multiple relations
if typeof relationHash.scope.include == "array"
_.each(relationHash.scope.include, (subRelationHash) ->
mixinCalcs(model[subRelationHash.relation](), subRelationHash)
)
else
mixinCalcs(model[relationHash.scope.include.relation](), relationHash.scope.include)
#iterate if the model to be unpacked is an array (toMany relationship)
if model[0]?
_.each(model, (subModel) ->
mixinCalcs(subModel, relationHash)
)
#we're done with this model, we don't want to mix anything into it
return
#check if the hash requests the inclusion of calcs
if relationHash.scope? and relationHash.scope.calc?
#setup deferreds because we will be loading things
iterationDeferred = Q.defer()
iterationDeferreds.push(iterationDeferred.promise)
calc = relationHash.scope.calc
#get the calcHash definition
calcHash = app.models[model.constructor.definition.name]["calcHash"]
#here we use a pair of deferreds. Inner deferrds load the reiquirements for each calculated val
#outer deferreds fire once all inner deferred deps are loaded to caluclate each val
#once all vals are calced the iteration deferred fires, resolving this object in the query
#once all iteration deferreds fire, we can send back the query through main deferred
outerDeferreds = new Array()
for k, v of calcHash
if calc[k]
((k, v) ->
outerDeferred = Q.defer()
outerDeferreds.push(outerDeferred.promise)
innerDeferreds = new Array()
#load each required relation, then resolve the inner promise
_.each(v.required, (req) ->
innerDeferred = Q.defer()
innerDeferreds.push(innerDeferred.promise)
model[req]((err, val) ->
console.log("inner Deferred for #{req} of #{model.constructor.definition.name}")
innerDeferred.resolve(val)
)
)
#all relations loaded, calculate the value and return it through outer deferred
Q.all(innerDeferreds).done((deps)->
ret = {}
ret[k] = v.fn(model, deps)
console.log("outer Deferred for #{k} of #{model.constructor.definition.name}")
outerDeferred.resolve(ret)
)
)(k, v)
#all calculations complete, mix them into the model
Q.all(outerDeferreds).done((deps)->
_.each(deps, (dep)->
for k, v of dep
model[k] = v
)
console.log("iteration Deferred for #{model.constructor.definition.name}")
iterationDeferred.resolve()
)
#/end iterate()
app.wrapper(model, fn, args).done((model) ->
mixinCalcs(model, {scope: args[0]})
console.log(iterationDeferreds)
#all models have been completed
Q.all(iterationDeferreds).done(()->
console.log("main Deferred")
mainDeferred.resolve(model)
)
)
return mainDeferred.promise
Compiled Javascript (without comments):
app.wrapper = function(model, fn, args) {
var deferred;
deferred = Q.defer();
args.push(function(err, result) {
if (err) {
console.log(err);
}
if (err) {
throw err;
}
return deferred.resolve(result);
});
app.models[model][fn].apply(app.models[model], args);
return deferred.promise;
};
app.mixCalcs = function(model, fn, args) {
var iterationDeferreds, mainDeferred, mixinCalcs;
mainDeferred = Q.defer();
iterationDeferreds = new Array();
mixinCalcs = function(model, relationHash) {
var calc, calcHash, iterationDeferred, k, outerDeferreds, v;
if ((relationHash.scope != null) && (relationHash.scope.include != null)) {
if (typeof relationHash.scope.include === "array") {
_.each(relationHash.scope.include, function(subRelationHash) {
return mixinCalcs(model[subRelationHash.relation](), subRelationHash);
});
} else {
mixinCalcs(model[relationHash.scope.include.relation](), relationHash.scope.include);
}
}
if (model[0] != null) {
_.each(model, function(subModel) {
return mixinCalcs(subModel, relationHash);
});
return;
}
if ((relationHash.scope != null) && (relationHash.scope.calc != null)) {
iterationDeferred = Q.defer();
iterationDeferreds.push(iterationDeferred.promise);
calc = relationHash.scope.calc;
calcHash = app.models[model.constructor.definition.name]["calcHash"];
outerDeferreds = new Array();
for (k in calcHash) {
v = calcHash[k];
if (calc[k]) {
(function(k, v) {
var innerDeferreds, outerDeferred;
outerDeferred = Q.defer();
outerDeferreds.push(outerDeferred.promise);
innerDeferreds = new Array();
_.each(v.required, function(req) {
var innerDeferred;
innerDeferred = Q.defer();
innerDeferreds.push(innerDeferred.promise);
return model[req](function(err, val) {
console.log("inner Deferred for " + req + " of " + model.constructor.definition.name);
return innerDeferred.resolve(val);
});
});
return Q.all(innerDeferreds).done(function(deps) {
var ret;
ret = {};
ret[k] = v.fn(model, deps);
console.log("outer Deferred for " + k + " of " + model.constructor.definition.name);
return outerDeferred.resolve(ret);
});
})(k, v);
}
}
return Q.all(outerDeferreds).done(function(deps) {
_.each(deps, function(dep) {
var _results;
_results = [];
for (k in dep) {
v = dep[k];
_results.push(model[k] = v);
}
return _results;
});
console.log("iteration Deferred for " + model.constructor.definition.name);
return iterationDeferred.resolve();
});
}
};
app.wrapper(model, fn, args).done(function(model) {
mixinCalcs(model, {
scope: args[0]
});
console.log(iterationDeferreds);
return Q.all(iterationDeferreds).done(function() {
console.log("main Deferred");
return mainDeferred.resolve(model);
});
});
return mainDeferred.promise;
};
The plugin depends on Q and underscore, so you'll need to include those libraries. The main code above should be loaded in the bootscript. Calculated properties are defined in the model's js definition file using the following syntax:
MODEL_NAME.calcHash = {
"ATTRIBUTE_NAME":
required: ["REQUIRED", "RELATION", "MODEL", "NAMES"]
fn: (model, deps) ->
#function which should return the calculated value. Loaded relations are provided as an array to the deps arg
return deps[0].value + deps[1].value + deps[2].value
"ATTRIBUTE_TWO":
#...
}
Call the plugin with the following syntax:
app.mixCalcs("MODEL_NAME", "FUNCTION_NAME (i.e. 'findOne')", [arguments for the called function])
Your filter now supports the property calc
which functions similarly to fields
, except it will include calculated attributes from the calcHash.
Example usage:
query = Candidate.app.mixCalcs("Candidate", "findOne", [{
where:
id: 1
include:
relation: "user"
scope:
calc:
timeSinceLastLogin: true
calc:
fullName: true
}])
query.done((result)->
cb(null, result)
)
It would be great if someone from the loopback team could incorporate a feature along these lines into the main release. I also opened a loopback issue.