Always fetch from related models in Bookshelf.js - node.js

I would like baffle.where({id: 1}).fetch() to always get typeName attribute as a part of baffle model, without fetching it from baffleType explicitly each time.
The following works for me but it seems that withRelated will load relations if baffle model is fetched directly, not by relation:
let baffle = bookshelf.Model.extend({
constructor: function() {
bookshelf.Model.apply(this, arguments);
this.on('fetching', function(model, attrs, options) {
options.withRelated = options.withRelated || [];
options.withRelated.push('type');
});
},
virtuals: {
typeName: {
get: function () {
return this.related('type').attributes.typeName;
}
}
},
type: function () {
return this.belongsTo(baffleType, 'type_id');
}
});
let baffleType = bookshelf.Model.extend({});
What is the proper way to do that?

Issue on repo is related to Fetched event, However Fetching event is working fine (v0.9.2).
So just for example if you have a 3rd model like
var Test = Bookshelf.model.extend({
tableName : 'test',
baffleField : function(){
return this.belongsTo(baffle)
}
})
and then do Test.forge().fetch({ withRelated : ['baffleField']}), fetching event on baffle will fire. However ORM will not include type (sub Related model) unless you specifically tell it to do so by
Test.forge().fetch({ withRelated : ['baffleField.type']})
However I would try to avoid this if it is making N Query for N records.
UPDATE 1
I was talking about same thing that you were doing on fetching event like
fetch: function fetch(options) {
var options = options || {}
options.withRelated = options.withRelated || [];
options.withRelated.push('type');
// Fetch uses all set attributes.
return this._doFetch(this.attributes, options);
}
in model.extend. However as you can see, this might fail on version changes.

This question is super old, but I'm answering anyway.
I solved this by just adding a new function, fetchFull, which keeps things pretty DRY.
let MyBaseModel = bookshelf.Model.extend({
fetchFull: function() {
let args;
if (this.constructor.withRelated) {
args = {withRelated: this.constructor.withRelated};
}
return this.fetch(args);
},
};
let MyModel = MyBaseModel.extend({
tableName: 'whatever',
}, {
withRelated: [
'relation1',
'relation1.related2'
]
}
);
Then whenever you're querying, you can either call Model.fetchFull() to load everything, or in cases where you don't want to take a performance hit, you can still resort to Model.fetch().

Related

Node.js Testing with Mongoose. unique gets ignored

I'm having a little trouble with an integration test for my mongoose application. The problem is, that my unique setting gets constantly ignored. The Schema looks more or less like this (so no fancy stuff in there)
const RealmSchema:Schema = new mongoose.Schema({
Title : {
type : String,
required : true,
unique : true
},
SchemaVersion : {
type : String,
default : SchemaVersion,
enum: [ SchemaVersion ]
}
}, {
timestamps : {
createdAt : "Created",
updatedAt : "Updated"
}
});
It looks like basically all the rules set in the schema are beeing ignored. I can pass in a Number/Boolean where string was required. The only thing that is working is fields that have not been declared in the schema won't be saved to the db
First probable cause:
I have the feeling, that it might have to do with the way I test. I have multiple integration tests. After each one my database gets dropped (so I have the same condition for every test and precondition the database in that test).
Is is possible that the reason is my indices beeing droped with the database and not beeing reinitiated when the next text creates database and collection again? And if this is the case, how could I make sure that after every test I get an empty database that still respects all my schema settings?
Second probable cause:
I'm using TypeScript in this project. Maybe there is something wrong in defining the Schema and the Model. This is what i do.
1. Create the Schema (code from above)
2. Create an Interface for the model (where IRealmM extends the Interface for the use in mongoose)
import { SpecificAttributeSelect } from "../classes/class.specificAttribute.Select";
import { SpecificAttributeText } from "../classes/class.specificAttribute.Text";
import { Document } from "mongoose";
interface IRealm{
Title : String;
Attributes : (SpecificAttributeSelect | SpecificAttributeText)[];
}
interface IRealmM extends IRealm, Document {
}
export { IRealm, IRealmM }
3. Create the model
import { RealmSchema } from '../schemas/schema.Realm';
import { Model } from 'mongoose';
import { IRealmM } from '../interfaces/interface.realm';
// Apply Authentication Plugin and create Model
const RealmModel:Model<IRealmM> = mongoose.model('realm', RealmSchema);
// Export the Model
export { RealmModel }
Unique options is not a validator. Check out this link from Mongoose docs.
OK i finally figured it out. The key issue is described here
Mongoose Unique index not working!
Solstice333 states in his answer that ensureIndex is deprecated (a warning I have been getting for some time now, I thought it was still working though)
After adding .createIndexes() to the model leaving me with the following code it works (at least as far as I'm not testing. More on that after the code)
// Apply Authentication Plugin and create Model
const RealmModel:Model<IRealmM> = mongoose.model('realm', RealmSchema);
RealmModel.createIndexes();
Now the problem with this will be that the indexes are beeing set when you're connection is first established, but not if you drop the database in your process (which at least for me occurs after every integration test)
So in my tests the resetDatabase function will look like this to make sure all the indexes are set
const resetDatabase = done => {
if(mongoose.connection.readyState === 1){
mongoose.connection.db.dropDatabase( async () => {
await resetIndexes(mongoose.models);
done();
});
} else {
mongoose.connection.once('open', () => {
mongoose.connection.db.dropDatabase( async () => {
await resetIndexes(mongoose.models);
done();
});
});
}
};
const resetIndexes = async (Models:Object) => {
let indexesReset: any[] = [];
for(let key in Models){
indexesReset.push(Models[key].createIndexes());
}
Promise.all(indexesReset).then( () => {
return true;
});
}

Dealing with geo queries on the client in the context of multiple subscriptions

I have two different subscriptions in my app:
Meteor.subscribe('collection');
and
Meteor.subscribe('filtered-collection',param1,param2);
I want to supply the data to different templates through different template helpers, say allResults and filteredResults respectively.
Since $geoWithin doesn't work at the client side and I need to use it for filtering, I cannot just filter the first subscription by
filteredResults = Collection.find(selector);`
Therefore, I need a separate subscription for it.
So, the question is: how to find the result set from respective subscription and pass it through a helper?
I finally solved the problem. I don't think the solution is ideal though.
At Server:
Collection = new Meteor.Collection('collection');
Meteor.publish('collection',function(){
return Collection.find();
});
Meteor.publish('filteredCollection',function(loc, radius){
var selector = {};
if (radius === undefined)
radius = 100;
if (loc !== undefined && !(isNaN(loc[0]) || isNaN(loc[1]))) {
selector.loc = {
$geoWithin: {
$centerSphere: [loc, radius / 6371]
}
};
}
var sub = this,
handle = null;
var handle = Collection.find(selector).observeChanges({
added: function(id, fields) {
sub.added("filteredCollection", id, fields);
},
changed: function(id, fields) {
sub.changed("filteredCollection", id, fields);
},
removed: function(id) {
sub.removed("filteredCollection", id);
}
});
sub.ready();
this.onStop(function() {
handle.stop();
});
});
At client:
Collection = new Meteor.Collection('collection');
FilteredCollection = new Meteor.Collection('filteredCollection');
Meteor.subscribe('collection');
Meteor.subscribe('filteredCollection',loc,radius);
Template.collection.helpers({
collection: function(){
return Collection.find();
},
filteredCollection: function(){
return FilteredCollection.find();
}
});
At the client, Collection and FilteredCollection are two different subsets of the same underlying collection at the server. But whether the two subsets are dependent on each other in terms of caching and persistence, is (I think) a different question altogether.

Sails.js/waterline self-referenced many to many two way association

I have the following kind of setup: I have a model called Person, which is related to other Persons (eg. neighbours) through a many-to-many association. The interpretation is that a person can have many neighbours, and a person can be the neighbour of many other people. The model attributes look just about like this:
...
attributes: {
name: 'string',
neigh: {
collection: 'Person',
via: 'neighOf',
dominant: true
},
neighOf: {
collection: 'Person',
via: 'neigh'
}
}
...
Now imagine us having two people, Person A and Person B. A is a neighbor of B, and B is a neighbor of A. A moves away, so they're no longer neighbors. Now when I remove B from the neighs of A, A will not automatically be removed from the neighs of B. This is a problem.
I tried to address the problem by creating an afterUpdate function to the Person model, in which I would check every person in the neighOf field of A for whether A still has them as a neighbour, and if not, would update the Persons that no longer were neighbours of A. I also wanted to do the opposite, so if A became a neighbour of B again (eg. the neigh field of B was updated), the neigh field of A should update automatically as well. Here's what I came up with:
afterUpdate: function(updatedPerson, cb) {
Person.findOne(updatedPerson.id)
.populate('neigh')
.populate('neighOf')
.then(function(person) {
updatedPerson = person;
var ids = _.pluck(updatedPerson.neigh, 'id');
return Person.find(ids).populate('neigh');
})
.then(function(people) {
neigh_of_updated = people;
var ids = _.pluck(updatedPerson.neighOf, 'id');
return Person.find(ids).populate('neigh');
})
.then(function(people) {
neighOf_of_updated = people;
var updates = [];
_.forEach(neighOf_of_updated, function(person) {
if (_.find(person.neigh, function(neigh) {return neigh.id === updatedPerson.id})
&& !_.find(updatedPerson.neigh, function(neigh) {return neigh.id === person.id})) {
var neighIds = _.pluck(person.neigh, 'id');
_.remove(neighIds, function(id) {
return id === updatedPerson.id;
});
updates.push(Person.update(person.id, {neigh: neighIds}));
}
})
return Promise.all(updates);
})
.then(function() {
var updates = [];
_.forEach(neighOfUpdated, function(person) {
if (!_.find(person.neigh, function(neigh) { return neigh.id === updatedPerson.id; })) {
var neighIds = _.pluck(person.neigh, 'id');
neighIds.push(updatedPerson.id);
updates.push(Person.update({id: Person.id}, {neigh: neighIds}))
}
})
return Promise.all(updates);
})
.then(function() {
cb();
})
.catch(function(error) {
cb(error);
})
}
So far I haven't been able to get both working. I can get them working separately, but when combined, the code is looping itself endlessly due to some updates happening in the middle of it all, causing new and new afterUpdates to be called.
I feel like this is a pretty common use case, so I'm guessing there should be an easier way to do all of this - or if there isn't, at least some way to get this working. As it is, I feel like I'm hitting my head against a brick wall in a matter that should be an easy task. Anyone have experience of similar associations and how to handle them?

loopbackjs: Attach a model to different datasources

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)
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
}
}
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.
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();
};
};

Overriding mongoose query for a specific model

I want to automatically add a query option for all queries related for a specific mongoose model without affecting other models
I saw this answer where Mongoose.Query is patched and that will affect all mongoose models.
I was able to do this for my soft deleted items. Haven't tested it extensively yet though.
function findNotDeletedMiddleware(next) {
this.where('deleted').equals(false);
next();
}
MySchema.pre('find', findNotDeletedMiddleware);
MySchema.pre('findOne', findNotDeletedMiddleware);
MySchema.pre('findOneAndUpdate', findNotDeletedMiddleware);
MySchema.pre('count', findNotDeletedMiddleware);
I see two possible easy ways to do this:
Alternative #1
Add a static dict with the options you want to be applied to your specific Mongoose schema:
FooSchema.statics.options = {
...
};
Now, when you query you need to do:
Foo.find({}, null, Foo.options, function(err, foos) {
...
});
Alternative #2
Implement a wrapper to the find method that always uses your specific options:
FooSchema.statics.findWithOptions = function(query, next) {
var options = { ... };
this.find(query, null, options, next);
};
And use this method like so:
Foo.findWithOptions({}, function(err, foos) {
...
})
Reusability
To make these wrapper methods more reusable, you can make a dict with all your wrappers:
var withOptionsWrappers = {
findWithOptions: function(query, next) {
this.find(query, null, this.options, next);
},
findByIdWithOptions: ...
findOneWithOptions: ...
...
};
Since we're referring to this there will be no problem reusing this. And now have this be applied to all your schemas along with your schema specific options:
FooSchema.statics = withOptionsWrappers;
FooSchema.statics.options = {
...
};
BarSchema.statics = withOptionsWrappers;
BarSchema.statics.options = {
...
};

Resources