Loopback - Include a Relation's Computed Properties
Asked Answered
H

3

1

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?

Hermaphroditus answered 3/3, 2015 at 19:49 Comment(0)
K
1

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

Kiangsu answered 29/8, 2015 at 12:7 Comment(0)
H
2

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.

Hermaphroditus answered 4/3, 2015 at 17:30 Comment(0)
K
1

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

Kiangsu answered 29/8, 2015 at 12:7 Comment(0)
S
0

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

Spurt answered 22/5, 2016 at 6:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.