Accessing `app` in a helper module

38 views
Skip to first unread message

Alex Reichert

unread,
Apr 18, 2017, 4:36:03 PM4/18/17
to LoopbackJS
I'm trying to determine the best way to access the `app` in a helper module. The loopback documentation suggests that it should be fine to simply `require('./server/server.js')`, but other threads imply that there may be issues with this approach (e.g. race conditions, potentially booting the app multiple times).

Here's what I'm trying to do in code:

In the Team model, I want to get the reporting data for the given teamId. To do this, I'm delegating to a helper module (ReportingApi) to handle that logic:
/* 
 * common/models/team.js
 */

// require helper module to generate reports
const ReportsApi = require('../../server/lib/reports-api.js');

module.exports = function(Team) {
  Team.getReports = (teamId, filter, cb) => {
    // ...

    ReportsApi.fetchStats(teamId, filter)
      .then(orders => cb(null, stats))
      .catch(err => cb(err));
  };

  Team.remoteMethod('getReportingOrders', {
    // ...
  });
};

Currently, that api looks something like this (where the `app` is just required at the top of the module)
/* 
 * server/lib/reports-api.js (Option #1)
 */

// require app directly from server.js (is this ok?)
const app = require('../../server.js');

const fetchStats = (teamId, filter) => {
  const { Order, Package, Etc } = app.models;

  return Promise.all([
    Order.find(...),
    Package.find(...),
    Etc.find(...)
  ])
    .then(results => {
      // generate reports with orders, packages, etc.
    });
};

module.exports = {
  fetchStats
};


But I'm wondering if it's better to pass the `app` into the module as a param, like so:
/* 
 * server/lib/reports-api.js (Option #2)
 */

// pass `app` as a param (is this preferable?)
const fetchStats = (app, teamId, filter) => {
  const { Order, Package, Etc } = app.models;

  return Promise.all([
    Order.find(...),
    Package.find(...),
    Etc.find(...)
  ])
    .then(results => {
      // generate reports with orders, packages, etc.
    });
};

module.exports = {
  fetchStats
};

Or even something like:
/* 
 * server/lib/reports-api.js (Option #1)
 */


class ReportsApi {
  // pass `app` into the constructor, i.e. new ReportsApi(app)
  constructor(app) {
    this.app = app;
  }

  fetchStats(teamId, filter) {
    const { Order, Package, Etc } = this.app.models;

    // generate reports with orders, packages, etc.
  }
}

module.exports = ReportsApi;

What is the preferred practice? To simply require `server/server.js` seems the most straightforward, I just wanted to make sure there's no harm in doing that.

Thanks!

Alex

Raymond Feng

unread,
Apr 18, 2017, 7:19:20 PM4/18/17
to loopb...@googlegroups.com
Alex,

I would go with either 2nd or 3rd option - passing `app` into the helper function.

Requiring `app` directly has some effects and it assumes the singleton `app`.

Thanks,
Raymond

--
You received this message because you are subscribed to the Google Groups "LoopbackJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to loopbackjs+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/loopbackjs/d19ec20f-6ce5-4715-9501-733df3bb28a7%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Alex Reichert

unread,
Apr 18, 2017, 7:28:47 PM4/18/17
to LoopbackJS
Thanks Raymond! Can you elaborate on what the effects are of requiring `app` directly?

Alex Reichert

unread,
Apr 18, 2017, 7:41:23 PM4/18/17
to LoopbackJS
Also just curious what approach you would take in this situation?

I can think of a few different ways of passing in the `app`:
// 1.
module.exports = function(app) {
 
const foo = () => app.models.Foo.find(...);
 
const bar = () => app.models.Bar.find(...);

 
return { foo, bar };
}

// 2.
const foo = (app) => app.models.Foo.find(...);
const bar = (app) => app.models.Bar.find(...);

module.exports = { foo, bar };

// 3.
module.exports = class Api {
  constructor
(app) {
   
this.app = app;
 
}

  foo
() {
   
return this.app.models.Foo.find(...);
 
}

  bar
() {
   
return this.app.models.Bar.find(...);
 
}
}

// 4.

// ???
 

Any suggestions?

Thanks,
Alex


On Tuesday, April 18, 2017 at 4:19:20 PM UTC-7, Raymond Feng wrote:

Joshua Mendoza

unread,
Apr 22, 2017, 1:41:45 AM4/22/17
to LoopbackJS
Hi Alex,

IMO I rather use a more functional style than relying on require.cache to obtain the app instance. Since I'm sometimes get wary with global state, I prefer to have control over the execution flow. Almost everything that needs to use LB in my codebase has some sort of app instance passing parameter, it may be like server/boot scripts' style with module.exports = app => { ... } or model extensions within server/models/<model>/<extension-n>.js getting the model like module.exports = <model> => { ... } and extracting instance directly from model with <model>.app

Sometimes we got issues extracting the app instance directly from the exported closure, so we moved the extraction directly into the extended methods. I think this is useful since app instance extraction is deferred until used in model's methods and not captured without change in the exported closure. I think I remember reading LB source code that the app instance is not a direct property of a model but something more complicated.

Moreover, passing app instance as a parameter gives you more control over the whole app' state when you do unit or integration testing.

It may be helpful to use ES6 classes to encapsulate the app instance but I have not so much experience with this lang feature, YMMV.



Finally, I'd rather leverage LB to take care of ReportsAPI as a private model in the system (server/model-config.json with public: false prop) and being able to fetch the model with const Report = Team.app.models.Report and then use some function of it, Report.fetchStats( ... ) (defined in server/models/report/methods.js). If you come to use different APIs for reports, make different models for them, like <ServiceA>Report, <ServiceB>Report, etc.


Disclaimer: This is a highly opinionated recommendation of how I would do these things, I do not know if any of what I said is "the best way" to do the job, but it 
has worked very well for me and my team, since it tries to be concise, leverages LB as a framework and keeps global state healthy.


I hope you find this info helpful.

Regards.
Reply all
Reply to author
Forward
0 new messages