How to update (override) a collection association's value in sails.js? - sails.js

I have a model that has an attribute that is a collection association:
Take for example, a User model below.
module.exports = {
attributes: {
pets: {
collection: 'pet'
}
}
}
I am aware that I can add pets to a user instance with
user.pets.add(3);
But how could I replace any existing pets with a new group of pets??

Ok I've been playing with the API and found an answer. The following call should update (set) the pets association for a single user. If there were existing pets, this approach would override them.
User.update({id:1}, {pets: [{id: 7}, {id: 8}]}).exec(cb);

You'd remove all the existing pets and create new ones. sails.js has no single special API function to do what you are trying to do, but it's pretty simple either way:
var newPets = [
{ name: 'fluffy', user: 1 },
...
];
Pet.destroy({ user: 1 })
.then(function () {
return _.map(newPets, Pet.create);
})
.then(function (pets) {
// pets are "replaced"
});
Or something like that.

Related

How to populate with a second model if first one is empty

I've three models,
modelA, modelB and ModelC
ModelC's data is here
{
"_id" : ObjectId("586e1661d9903c6027a3b47c"),
"RefModel" : "modelA",
"RefId" : ObjectId("57f37f18517f72bc09ee7632")
},
{
"_id" : ObjectId("586e1661d9903c6027a3b47c"),
"RefModel" : "modelB",
"RefId" : ObjectId("57f37f18517f72bc09ee7698")
},
Howto write a populate query for ModelC, to populate with RefId .
it should populate with modelA or modelB which RefModel refers.
I've tried with
ModelC.find({})
.populate({ path: 'RefId', model: 'modelA' })
.populate({ path: 'RefId', model: 'modelB' })
But taking only the last model.
modelC schema.
new mongoose.Schema({
RefModel: String,
RefId:{ type: Schema.ObjectId}});
I could do it with aggregate, but preferring populate.
Fields' names in your database and schema are very confusing, let me explain on more clear example.
Suppose you have 3 models: User, Article and Comment. Article and Comment belongs only to single User. User can have multiple comments and articles (As you shown in your example).
(More efficient and reccommending way). Store comments and articles ids in User model like:
comments: [{ id: '..', ref: Comment }],
articles: [{ id: '..', ref: Article }]
and populate your document:
User.find({})
.populate('comments')
.populate('articles');
Store User id in Comment and Article models like
user: { id: '...', ref: User }
and use mongoose-reverse-populate module for population, for example for comments model:
var reversePopulate = require('mongoose-reverse-populate');
User.find().exec(function(err, users) {
var opts = {
modelArray: users,
storeWhere: "comments",
arrayPop: true,
mongooseModel: Comments,
idField: "user"
}
reversePopulate(opts, function(err, popUsers) {
// populated users will be populated with comments under .comments property
});
});
Also you dont need to keep RefModel in your database, only in schema.
Here, you can try and populate it inside the callback of find itself.
Try this:
ModelC.find({}).exec(function(err,modelC){
//find returns an array of Object, you will have to populate each Object individually.
var modelCObjects =[];
modelC.forEach(function(tempModelC){
//populate via the Model
ModelC.populate(tempModelC,{path : tempModelC.refId , model :tempModelC.refModel},function(err2,newModelC){
//handle newModelC however you want
//Push it into array of modelC Objects
modelCObjects.push(newModelC);
});
//OR
//Populate directly on the resultObject <-im not so sure about this
tempModelC.populate({path : tempModelC.refId , model :tempModelC.refModel},function(err2,newModelC){
//Push it into array of modelC Objects
modelCObjects.push(newModelC);
})
});
//here you have modelCObjects -> Array of populated modelC Objects
});
Please make sure to handle the errors.

Updating association in Sails

I am using SailsJs. Two models exist. Model 'person' contains the following attribute:
books: {
collection: 'book',
via: 'owner'
},
Model 'book' contains:
owner: {
model: 'person'
},
When I create an instance of person, I can use http request and simply put something like the following
Post /person?books=1001,1002
As long as 1001 and 1002 are valid ids of book, it works.
However, when I try to person's books attribute, this does not work
Post /person/1?books=1002,1003
books of person with id 1 becomes empty.
But,
Post /person/1?books=1002
would work.
Why is this? How can I modify collection attribute?
There are 2 options to update a model using the blueprints API:
Send the data as the body of the post request. send the data as a json object in the request body
Post /person/1
body: { "books": [1002,1003]}
Override the blueprint update method
Add this the PersonController.js:
update: function (req, res) {
Person.findOne(req.param('id')).populate('books').exec(function (err, person){
if (err) {
sails.log.error(err);
return;
}
var books = req.param('books').split(',').map(function (book){return parseInt(book);}),
booksToRemove = _.difference(person.books.map(function(book) {return book.id;}), books);
// Remove books that are not in the requested list
person.books.remove(booksToRemove);
// Add new books to the list
person.books.add(books);
person.save(function (err, r) {;
res.json(r);
});
});
}

sailsjs / waterline query "where" not empty

Hy there,
Before going to the hacky / cutom way i wanted to know if there is a built in query way to check for an empty / non empty many to many relationship as i was not successfull neither on google nor the doc.
If i take the example in the doc let's imagine i want to retrive a user only if he has a a Pet or Retrive a Pet without any Owner through a query.
// A user may have many pets
var User = Waterline.Collection.extend({
identity: 'user',
connection: 'local-postgresql',
attributes: {
firstName: 'string',
lastName: 'string',
// Add a reference to Pet
pets: {
collection: 'pet',
via: 'owners',
dominant: true
}
}
});
// A pet may have many owners
var Pet = Waterline.Collection.extend({
identity: 'pet',
connection: 'local-postgresql',
attributes: {
breed: 'string',
type: 'string',
name: 'string',
// Add a reference to User
owners: {
collection: 'user',
via: 'pets'
}
}
});
P.s. i know how to filter results after query execution that's not what i'm asking :)
There is nothing built in (aka User.hasPet() ) or something like that., so the simple answer is NO
If I know of these issues before hand I tend to write my DB in such a way that the queries are fast. IE: the User schema would have a hasPets column. Whenever a pet is added/removed a callbacks hits the user table to mark that field if it has an owner or not. So then I can query User.findAll({hasPet:true}).
Its a little much, but it depends on where you speed is needed.
This is a bit late, but I wanted to let you know it's pretty easy to do this with the Waterline ORM lifecycle functions. I've done it in a few of my projects. Basically, use the beforeCreate and beforeUpdate functions to set your flags. For your user, it might look like...
var User = Waterline.Collection.extend({
identity: 'user',
connection: 'local-postgresql',
beforeCreate: function(values, next) {
if (values.pets) {
values.has_pet = true;
} else {
values.has_pet = false;
}
next();
}
beforeUpdate: function(values, next) {
if (values.pets) {
values.has_pet = true;
} else {
values.has_pet = false;
}
next();
}
attributes: {
firstName: 'string',
lastName: 'string',
// Add a reference to Pet
pets: {
collection: 'pet',
via: 'owners',
dominant: true
},
has_pet: {
type: 'boolean'
}
}
});
Then you can query based on the has_pet attribute

Sailsjs add one to many association to a model during beforeCreate

I am trying to give a default association from a user to a pet, whenever a new User created.
Model:: User.js
var User = {
attributes: {
name: {type: 'string'},
// Add a One Way Relation to pet model
pets: {
collection: 'pet'
},
},
/*** This did not work ***/
beforeCreate: function (user, next) {
var defaultPet = {name: 'Default Pet 1'};
Pet.find(defaultPet).exec(function(err, pet) {
user.name = "BEFORECREATE",
user.pets = pet[0].id;
next();
});
},
}
module.exports = User;
However when a new record is created the user.pet is [ ], but user.name is changed to "BEFORECREATE".
How do I get user.pets = [{name: 'Default Pet 1'}] automatically for the new user created?
Or is there a better place for setting such defaults?
----- UPDATE: More info
I am using sails-disk currently.
Model: Pet.js
module.exports = {
attributes: {
name: {
type: 'string',
required: true
}
}
};
You can't add associations to a model in a lifecycle callback like beforeCreate. Currently, Waterline looks for and processes "nested models" like these before lifecycle callbacks run, so by the time beforeCreate is called it's too late. The simplest solution would be to create a User.createUser class method that wraps the logic you want:
createUser: function(data, cb) {
// If there are already pets specified, just use those
if (data.pets) {return User.create(data).exec(cb);}
// Otherwise look up the default pet
Pet.findOne({name:"Default Pet 1"}).exec(function(err,pet) {
// Return in case of error
if (err) {return cb(err);}
// Assuming the default pet exists, attach it
if (pet) {
console.log("SETTING DEFAULT PET", pet.id);
data.pets = [pet.id];
}
// Create the pet
return User.create(data).exec(cb);
});
}
A few notes:
In your example you were setting pets directly to an ID, but since it's a collection you must set it to an array.
If you're using the sails-disk adapter, you'll need to set schema: true in your model for this to work.
The new User model you get back will not be populated; you'll have to do a find with a populate('pets') with the new User ID to get the pet data attached.

How do I use new Meteor.Collection.ObjectID() in my mongo queries with meteor?

I have a Collection that has documents with an array of nested objects.
Here is fixture code to populate the database:
if (Parents.find().count() == 0) {
var parentId = Parents.insert({
name: "Parent One"
});
Children.insert({
parent: parentId,
fields: [
{
_id: new Meteor.Collection.ObjectID(),
position: 3,
name: "three"
},
{
_id: new Meteor.Collection.ObjectID(),
position: 1,
name: "one"
},
{
_id: new Meteor.Collection.ObjectID(),
position: 2,
name: "two"
},
]
});
}
You might be asking yourself, why do I even need an ObjectID when I can just update based off of the names. This is a simplified example to a much more complex schema that I'm currently working on and the the nested object are going to be created dynamically, the ObjectID's are definitely going to be necessary to make this work.
Basically, I need a way to save those nested objects with a unique ID and be able to update the fields by their _id.
Here is my Method, and the call I'm making from the browser console:
Meteor.methods({
upChild: function( options ) {
console.log(new Meteor.Collection.ObjectID());
Children.update({_id: options._id, "fields._id": options.fieldId }, {$set: {"fields.$.position": options.position}}, function(error){
if(error) {
console.log(error);
} else {
console.log("success");
}
});
}
});
My call from the console:
Meteor.call('upChild', {
_id: "5NuiSNQdNcZwau92M",
fieldId: "9b93aa1ef3868d762b84d2f2",
position: 1
});
And here is a screenshot of the html where I'm rendering all of the data for the Parents and Children collections:
Just an observation, as I was looking how generate unique IDs client side for a similar reason. I found calling new Meteor.Collection.ObjectID() was returning a object in the form 'ObjectID("abc...")'. By assigning Meteor.Collection.ObjectID()._str to _id, I got string as 'abc...' instead, which is what I wanted.
I hope this helps, and I'd be curious to know if anyone has a better way of handling this?
Jason
Avoid using the _str because it can change in the future. Use this:
new Meteor.Collection.ObjectID().toHexString() or
new Meteor.Collection.ObjectID().valueOf()
You can also use the official random package:
Random.id()