Reputation: 2385
I have a loopback app and I'd like to be able to include computed properties from relations in an API call. For example, say I have an apartment
model and an address
model. The address
has properties city
and state
.
I'd like to make one call, to the apartment model, and include the city and state as a single string from the related address
model.
I took some inspiration from @Raymond Feng's answer to this question, and tried the following approach (excuse the coffeescript/pseudo-code):
address.defineProperty(address.prototype, "fullAddress",
get: () -> return address.city + " " + address.state
)
However, when I try:
apartment.findOne({
include:
relation: "address"
scope:
fields:
fullAddress: true
}, (err, apartment) ->
console.log(apartment)
)
I get
Error: ER_BAD_FIELD_ERROR: Unknown column 'fullAddress' in 'field list'
Notably, when I try to query the address model without specifying fields, I get an attribute named '[object Object]' with a value of null, which I suspect is a result of my attempt to define the fullAddress
property.
I assume that I'm approaching the problem with the wrong syntax. Is what I am looking for possible, and if so, how do I do it?
Upvotes: 1
Views: 2974
Reputation: 141
You can try some great mixins for that, here is my collection:
You can try 3rd party plugins:
1) Loopback connector for aggregation: https://github.com/benkroeger/loopback-connector-aggregate
2) Loopback mixins for computed/calculated properties (works only when new model instance created): https://github.com/fullcube/loopback-ds-calculated-mixin https://github.com/fullcube/loopback-ds-computed-mixin
3) Loopback mixin for change tracking (launches on every update): https://github.com/fullcube/loopback-ds-changed-mixin
4) If you need a stats - here is another mixin: https://github.com/jonathan-casarrubias/loopback-stats-mixin
5) You can count related models: https://github.com/exromany/loopback-counts-mixin
6) You can automaticly denormalize and save related data and choose what fields will be stored (useful for caching): https://github.com/jbmarchetti/loopback-denormalize
7) If you need a computed properties for field mapping during import: https://github.com/jonathan-casarrubias/loopback-import-mixin
Upvotes: 0
Reputation: 885
It is not true (anymore) that Loopback doesn't support calculated properties.
This can be done with Loopback's operational hooks as I described here: Dynamic Properties or Aggregate Functions in Loopback Models
Upvotes: 1
Reputation: 2385
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.
Upvotes: 2