loopbackjs: Attach a model to different datasources
Asked Answered
H

3

9

I have defined several models that use a Datasource "db" (mysql) for my environment.

Is there any way to have several datasources attached to those models, so I would be able to perform REST operations to different databases?

i.e: GET /api/Things?ds="db"

GET /api/Things?ds="anotherdb"

GET /api/Things (will use default ds)

Horripilate answered 25/11, 2014 at 10:44 Comment(0)
S
8

As @superkhau pointed above, each LoopBack Model can be attached to a single data-source only.

You can create (subclass) a new model for each datasource you want to use. Then you can either expose these per-datasource models via unique REST URLs, or you can implement a wrapper model that will dispatch methods to the correct datasource-specific model.

In my example, I'll show how to expose per-datasource models for a Car model that is attached to db and anotherdb. The Car model is defined in the usual way via common/models/car.json and common/models/car.js.

Now you need to define per-datasource models:

// common/models/car-db.js
{
  "name": "Car-db",
  "base": "Car",
  "http": {
    "path": "/cars:db"
  }
}

// common/models/car-anotherdb.js
{
  "name": "Car-anotherdb",
  "base": "Car",
  "http": {
    "path": "/cars:anotherdb"
  }

}

// server/model-config.json
{
  "Car": {
    "dataSource": "default"
  },
  "Car-db": {
    "dataSource": "db"
  },
  "Car-anotherdb": {
    "dataSource": "anotherdb"
  }
}

Now you have the following URLs available:

GET /api/Cars:db
GET /api/Cars:anotherdb
GET /api/Cars

The solution outlined above has two limitations: you have to define a new model for each datasource and the datasource cannot be selected using a query parameter.

To fix that, you need a different approach. I'll again assume there is a Car model already defined.

Now you need to create a "dispatcher".

// common/models/car-dispatcher.json
{
  "name": "CarDispatcher",
  "base": "Model", //< important!
  "http": {
    "path": "/cars"
  }
}

// common/models/car-dispatcher.js
var loopback = require('loopback').PersistedModel;
module.exports = function(CarDispatcher) {
  Car.find = function(ds, filter, cb) {
    var model = this.findModelForDataSource(ds);
    model.find(filter, cb);
  };

  // a modified copy of remoting metadata from loopback/lib/persisted-model.js
  Car.remoteMethod('find', {
    isStatic: true,
    description: 'Find all instances of the model matched by filter from the data source',
    accessType: 'READ',
    accepts: [
     {arg: 'ds', type: 'string', description: 'Name of the datasource to use' },
     {arg: 'filter', type: 'object', description: 'Filter defining fields, where, orderBy, offset, and limit'}
    ],
    returns: {arg: 'data', type: [typeName], root: true},
    http: {verb: 'get', path: '/'}
  });

  // TODO: repeat the above for all methods you want to expose this way

  Car.findModelForDataSource = function(ds) {
    var app = this.app;
    var ds = ds && app.dataSources[ds] || app.dataSources.default;

    var modelName = this.modelName + '-' + ds;
    var model = loopback.findModel(modelName);
    if (!model) {
      model = loopback.createModel(
        modelName, 
        {},
        { base: this.modelName });
    }

    return model;
  };  
};

The final bit is to remove Car and use CarDispatcher in the model config:

// server/model-config.json
{
  "CarDispatcher": {
    dataSource: null,
    public: true
  }
}
Schnitzler answered 4/2, 2015 at 17:10 Comment(1)
I followed your steps, but it seems that there's a missing piece, deleting the Car model from model-config.js crashes my server with an error saying Test is not definedSolley
B
1

By default, you can only attach data sources on a per-model basis. Meaning you can attach each model to a different data source via datasources.json.

For your use case, you will to add a remote hook to each endpoint you want for multiple data sources. In your remote hook, you will do something like:

...
var ds1 = Model.app.dataSources.ds1;
var ds2 = Model.app.dataSources.ds2;

//some logic to pick a data source
if (context.req.params...
...

See http://docs.strongloop.com/display/LB/Remote+hooks for more info.

Bield answered 25/11, 2014 at 18:47 Comment(0)
I
0

For anyone still looking for a working answer to this, the solution for switching databases on the fly was to write a middleware script that examined the request path and then created a new DataSource connector, passing in a variable based on the req.path variable. For example, if the request path is /orders, then "orders" as a string would be saved in a variable, then we attached a new Datasource, passing in that variable for "orders". Here's the complete working code.

'use strict';

const DataSource = require('loopback-datasource-juggler').DataSource;
const app = require('../server.js');

module.exports = function() {
  return function datasourceSelector(req, res, next) {
  // Check if the API request path contains one of our models.
  // We could use app.models() here, but that would also include
  // models we don't want.
  let $models = ['offers', 'orders', 'prducts'];
  // $path expects to be 'offers', 'orders', 'prducts'.
  let $path = req.path.toLowerCase().split("/")[1];

  // Run our function if the request path is equal to one of
  // our models, but not if it also includes 'count'. We don't
  // want to run this twice unnecessarily.
  if (($models.includes($path, 0)) && !(req.path.includes('count'))) {
    // The angular customer-select form adds a true value
    // to the selected property of only one customer model.
    // So we search the customers for that 'selected' = true.
    let customers = app.models.Customer;
    // Customers.find() returns a Promise, so we need to get
    // our selected customer from the results.
    customers.find({"where": {"selected": true}}).then(function(result){
      // Called if the operation succeeds.
      let customerDb = result[0].name;
      // Log the selected customer and the timestamp
      // it was selected. Needed for debugging and optimization.
      let date = new Date;
      console.log(customerDb, $path+req.path, date);
      // Use the existing veracore datasource config
      // since we can use its environment variables.
      let settings = app.dataSources.Veracore.settings;
      // Clear out the veracore options array since that
      // prevents us from changing databases.
      settings.options = null;
      // Add the selected customer to the new database value.
      settings.database = customerDb;
      try {
        let dataSource = new DataSource(settings);
        // Attach our models to the new database selection.
        app.models.Offer.attachTo(dataSource);
        app.models.Order.attachTo(dataSource);
        app.models.Prduct.attachTo(dataSource);
      } catch(err) {
        console.error(err);
      }
    })
    // Called if the customers.find() promise fails.
    .catch(function(err){
      console.error(err);
    });
  }
  else {
    //  We need a better solution for paths like '/orders/count'.
    console.log(req.path + ' was passed to datasourceSelector().');
  }
  next();
  };
};
Idette answered 4/2, 2019 at 15:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.