Can a Waterline model access its collection in life-cycle events? - sails.js

I have a model where only one record can have a 'current' property set to, for example, 1.
Is it possible for the beforeCreate or beforeUpdate to access the collection. Basically I want to do something like this:
afterUpdate: function (values, next) {
// If this value is current, reset all the others
if (values.current == 1) {
this.collection.update({
id: { '!': values.id }
}, {
current: 0
}, next);
}
}
What id don't know is what I can reliably use for this.collection in the example above.
Thanks in advance.

Yes, if you're using Sails v0.10.X you can access any model using sails global variable:
sails.models.users.find({id: 1})
Note: all model names in sails.models are in lower case

Related

Where to put version __v in redux state?

I have a model that is scattered all around the application. I have a redux state tree:
{
page: {
modelPart1: ...,
... : {
modelPart2: ...
}
}
I need to keep a reference to mongoDb __v in my state too. Where is the best place to place it?
I was thinking about a separate branch model_metadata that would keep the metadata about docs (_id, __v, ...).
{
model_metadata: { <------------------------ HERE
model: {
_id: id,
__v: 2
}
}
page: {
modelPart1: ...,
... : {
modelPart2: ...
}
}
Is it a valid approach or would you recommend a different one?
Every reducer only can access its own part of state, so when you do
combineReducers({
one,
another
});
and access state in one, it is equivalent to doing store.getState().one, and the same for another. So, you need to split the data in page property of state into two parts: actual data and metadata. Just like the object you retrieve from Mongo.
The point in having metadata and actual data being processed by the same reducer is that every time a reducer function is performed, you have everything you need about your object in state argument of that function. Splitting the data into two different reducers would make things way more complicated.
So, the new data representation in page would look like
{
model_metadata: { <------------------------ HERE
model: {
_id: id,
__v: 2
}
}
page: {
modelPart1: ...,
... : {
modelPart2: ...
}
}
while connecting to page would look like
connect(state => ({
page: state.page
})(...)

Query sailsjs blueprint endpoints by id array using request

I'm using the request library to make calls from one sails app to another one which exposes the default blueprint endpoints. It works fine when I query by non-id fields, but I need to run some queries by passing id arrays. The problem is that the moment you provide an id, only the first id is considered, effectively not allowing this kind of query.
Is there a way to get around this? I could switch over to another attribute if all else fails but I need to know if there is a proper way around this.
Here's how I'm querying:
var idArr = [];//array of ids
var queryParams = { id: idArr };
var options: {
//headers, method and url here
json: queryParams
};
request(options, function(err, response, body){
if (err) return next(err);
return next(null, body);
});
Thanks in advance.
Sails blueprint APIs allow you to use the same waterline query langauge that you would otherwise use in code.
You can directly pass the array of id's in the get call to receive the objects as follows
GET /city?where={"id":[1, 2]}
Refer here for more.
Have fun!
Alright, I switched to a hacky solution to get moving.
For all models that needed querying by id arrays, I added a secondary attribute to the model. Let's call it code. Then, in afterCreate(), I updated code and set it equal to the id. This incurs an additional database call, but it's fine since it's called just once - when the object is created.
Here's the code.
module.exports = {
attributes: {
code: {
type: 'string'//the secondary attribute
},
// other attributes
},
afterCreate: function (newObj, next) {
Model.update({ id: newObj.id }, { code: newObj.id }, next);
}
}
Note that newObj isn't a Model object as even I was led to believe. So we cannot simply update its code and call newObj.save().
After this, in the queries having id arrays, substituting id with code makes them work as expected!

How to call cascading find() in Meteor + MongoDB?

I use Meteor & Iron-router. I have the following data context defined in the router:
data: function() { return {
eventId: this.params._id,
registrants: Registrants.find({eventIds: {$elemMatch: { $in: [this.params._id]}}}, {sort: {name:1, phone:1, email:1}}),
}}
I want to enable Registrants to be filtered further by user input. In my case, I already have ReactiveVar called filterName which listen to input text from user. Whenever the input text changed, the filterName is updated. ( I followed this answer ng-repeat + filter like feature in Meteor Blaze/Spacebars)
Now, I want to add $and to the Registrants.find() method to derive new registrants data context. How should I do it so that the query is reactive to the filterName?
Another approach is by defining Template helper method filteredRegistrants. Initially, its value is the same as return this.registrants. Whenever filterName changed, I would do return this.registrants.find({name: filterName}), but somehow I can't invoke find from registrants cursor, can I? I got undefined is not function error when doing that.
this.registrants is already a cursor (result of Registrants.find()), and not a collection, thus it doesn't have the find() method you look for. However, there is nothing wrong with making another query in the helper if the functionality provided by your controller is not enough:
Template.registrantsTemplate.helpers({
filteredRegistrants: function() {
return Registrants.find(...query...);
},
});

Subscribing to Meteor.Users Collection

// in server.js
Meteor.publish("directory", function () {
return Meteor.users.find({}, {fields: {emails: 1, profile: 1}});
});
// in client.js
Meteor.subscribe("directory");
I want to now get the directory listings queried from the client like directory.findOne() from the browser's console. //Testing purposes
Doing directory=Meteor.subscribe('directory')/directory=Meteor.Collection('directory') and performing directory.findOne() doesn't work but when I do directory=new Meteor.Collection('directory') it works and returns undefined and I bet it CREATES a mongo collection on the server which I don't like because USER collection already exists and it points to a new Collection rather than the USER collection.
NOTE: I don't wanna mess with how Meteor.users collection handles its function... I just want to retrieve some specific data from it using a different handle that will only return the specified fields and not to override its default function...
Ex:
Meteor.users.findOne() // will return the currentLoggedIn users data
directory.findOne() // will return different fields taken from Meteor.users collection.
If you want this setup to work, you need to do the following:
Meteor.publish('thisNameDoesNotMatter', function () {
var self = this;
var handle = Meteor.users.find({}, {
fields: {emails: 1, profile: 1}
}).observeChanges({
added: function (id, fields) {
self.added('thisNameMatters', id, fields);
},
changed: function (id, fields) {
self.changed('thisNameMatters', id, fields);
},
removed: function (id) {
self.removed('thisNameMatters', id);
}
});
self.ready();
self.onStop(function () {
handle.stop();
});
});
No on the client side you need to define a client-side-only collection:
directories = new Meteor.Collection('thisNameMatters');
and subscribe to the corresponding data set:
Meteor.subscribe('thisNameDoesNotMatter');
This should work now. Let me know if you think this explanation is not clear enough.
EDIT
Here, the self.added/changed/removed methods act more or less as an event dispatcher. Briefly speaking they give instructions to every client who called
Meteor.subscribe('thisNameDoesNotMatter');
about the updates that should be applied on the client's collection named thisNameMatters assuming that this collection exists. The name - passed as the first parameter - can be chosen almost arbitrarily, but if there's no corresponding collection on the client side all the updates will be ignored. Note that this collection can be client-side-only, so it does not necessarily have to correspond to a "real" collection in your database.
Returning a cursor from your publish method it's only a shortcut for the above code, with the only difference that the name of an actual collection is used instead of our theNameMatters. This mechanism actually allows you to create as many "mirrors" of your datasets as you wish. In some situations this might be quite useful. The only problem is that these "collections" will be read-only (which totally make sense BTW) because if they're not defined on the server the corresponding `insert/update/remove' methods do not exist.
The collection is called Meteor.users and there is no need to declare a new one on neither the server nor the client.
Your publish/subscribe code is correct:
// in server.js
Meteor.publish("directory", function () {
return Meteor.users.find({}, {fields: {emails: 1, profile: 1}});
});
// in client.js
Meteor.subscribe("directory");
To access documents in the users collection that have been published by the server you need to do something like this:
var usersArray = Meteor.users.find().fetch();
or
var oneUser = Meteor.users.findOne();

Sails.js expireAfterSeconds option in the model

I am using sails to write a simple model which should expire after a few hours, so I need something like
createdAt: {
type: 'Date',
expires : 60,
index: true
}
But the "expireAfterSeconds" seems not working when I check my database (MongoDB), therefore I have to use
db.collection.ensureIndex( { "createdAt": 1 }, { expireAfterSeconds: 3600 } )
I wonder if it is possible to set the "expire" option in the model?
Inspecting the sails-mongo source code that is responsible for handling the index attribute property[1], it seems that it does not take into account anything like an expires property. This makes sense though, because the sails.js waterline ORM does not support database-specific functionality like expiring index data in MongoDB.
However, waterline does provide access to the MongoDB native connection (which uses node-mongodb-native) through the Collection.native() method[2]
Chances are, you only really need to make changes to the model's index once during the lifecycle of the Sails application, so the best place to do that would be in the config/bootstrap.js file. At this point in the sails.js lifecycle, all of the models have been instantiated, so you could do something like this to perform the necessary logic on your keys:
// config/bootstrap.js
exports.bootstrap = function (done) {
YourModelName.native(function (err, collection) {
// define index properties
collection.ensureIndex( { "createdAt": 1 }, { expireAfterSeconds: 3600 } );
// be sure to call the bootstrap callback to indicate completion
done();
});
}
You could take this even further and write a utility that inspects the models that sails has loaded (they are available as keys in the global object sails.models) and, if they have an expires attribute, perform the necessary native functionality. This would also need to be done in the config/bootstrap.js.
If you have native functionality that needs to be done on a per-record basis, you can use the sails.js model lifecycle hooks[3] for that.
references:
[1] https://github.com/balderdashy/sails-mongo/blob/ad7ec276af3a0e823ee5075074788ca915328db2/lib/adapter.js#L112
[2] https://github.com/balderdashy/sails-mongo/issues/21#issuecomment-20765896
[3] See the section titled "Lifecycle Callbacks" # http://sailsjs.org/#!documentation/models
UPDATE:
I created a sails hook to give advanced indexing options for models that use the sails-mongo adapter.
Supports all mongo indexing options.
https://www.npmjs.com/package/sails-hook-mongoat
OLD:
I created the below to do this automatically if the model attribute has an expires property and is of type 'date'. Just paste into to your config/bootstrap.js
Hope it helps!
_.forEach(Object.keys(sails.models), function(key) {
var model = sails.models[key];
_.forEach(Object.keys(model.attributes), function(attr) {
if (model.attributes[attr].hasOwnProperty('expires')) {
var seconds = model.attributes[attr].expires;
// delete validators from global model, otherwise sails will error
delete sails.models[key].attributes[attr].expires;
delete sails.models[key]._validator.validations[attr].expires;
if (model.attributes[attr].hasOwnProperty('type') && model.attributes[attr].type === 'date') {
var obj = {};
obj[attr] = 1;
model.native(function(err, collection) {
// define index properties
collection.ensureIndex(obj, {
expireAfterSeconds: seconds
}, function(err) {
if (err)
sails.log.error(err);
else
sails.log.info(key + " model attribute '" + attr + "' has been set to expire the document after " + seconds + " seconds.");
});
});
} else {
sails.log.warn(key + " model attribute '" + attr + "' is set to expire but is not of type 'date'. Skipping...");
}
}
});
});