Sailsjs add one to many association to a model during beforeCreate - sails.js

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.

Related

Create unique multikey index via model settings

I am using Sails v1.1 -
I created a many-to-many through custom model association following the sails doc here - https://sailsjs.com/documentation/concepts/models-and-orm/associations/through-associations
The PetUser model has two columns pet and user, where each is the respective id. I want to create a unique multi-key index, meaning there cannot be two rows with the same combination of "pet and user". Meaning the second call should succeed, and third call should fail with uniqueness error:
await PetUser.create({ user: 1, pet: 33 }); // should succeed
await PetUser.create({ user: 1, pet: 44 }); // should succeed as user/pet combination is different
await PetUser.create({ user: 1, pet: 33 }); // should fail
I tried adding unique: true to both the owner and pet attribute on PetUser model below, but only the first unique: true gets respected.
So this is my code in myApp/api/models/PetUser.js
module.exports = {
attributes: {
owner: {
model:'user',
unique: true
},
pet: {
model: 'pet',
unique: true
}
}
}
For implementing similar behavior I added a combined attribute and mark it unique. Also, I added beforeCreate and beforeUpdate model hooks on which I generate my combined attribute to check is it unique or not.
const YourModel = {
attributes: {
owner: {
model: 'user',
},
pet: {
model: 'pet',
},
petOwner: {
type: 'string',
unique: true,
}
},
beforeCreate : function(values,cb) {
// TODO get ids from related records or reset to default on missed relation record if you need it
const petId = 35;
const ownerId = 8;
values.petOwner = `${petId}-${ownerId}`;
cb();
},
beforeUpdate : function(values,cb) {
YourModel.beforeCreate(values, cb)
},
};
module.exports = YourModel;
In result when you tries to add the record with the same relations, you will get E_UNIQUE as you expected.

How to have an optional association using Waterline?

I'm using sails.js for a project and everything is going fine so far. Except that I don't know how to have an optional association between my two models. If I don't specify one, then if I use populate() it takes the first one available.
I have those two models:
// Book.js
module.exports = {
attributes: {
title: 'string',
serie: { model: 'serie' }
},
};
// Serie.js
module.exports = {
attributes: {
name: 'string',
books: { collection: 'book', via: 'serie' }
}
};
If I do this:
$ sails console
> Book.create({title: "Title"}).exec(function(err, book) {
Book.findOne({id: book.id }).populateAll().exec(function(err, book) {
console.log(book);
});
});
I get this:
{
serie: { name: 'Previously inserted serie' },
title: 'Title',
id: '55d6230122e3b1e70d877351'
}
Why isn't serie empty ? When inserting the book, I didn't specify any serie but it is still linked to a random one.
It was actually a bug from the sails-mongo adapter. I made a pull request that fixes it.

How to update (override) a collection association's value in 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.

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

Why does Sails v.0.10.x respond to POST to nested route with one-to-many association?

In my app, I have two models Person and Room. I also have two controllers, PersonController and RoomController. I have added no custom routes, and I've overridden none of the boilerplate controller actions.
When I POST to, for example, /room/5/person, a person record is created and associated with room/5 as expected - great. However, the JSON returned in the response to this POST contains the Room and all People associated with the room, side-loaded in an array. I expected that Sails would return only the person created (or, at most, the newly created person and the room, if blueprints.populate: true).
What changes to I need to make to so that Sails returns only the Person created as a result of the POST?
Here's is my current code:
/api/models/Room.js
// Room.js
module.exports = {
autosubscribe: ['update', 'add:people'],
attributes: {
name: 'string',
// Has-many association
// A room can have many people
people: {
collection: 'person',
via: 'room'
}
}
};
/api/models/Person.js
module.exports = {
autosubscribe: ['update'],
attributes: {
name: 'string',
// Belongs-to association
// A person can belong to only one room
room: {
model: 'room'
}
}
};
/api/controllers/RoomController.js
module.exports = {
};
/api/controllers/PersonController.js
module.exports = {
};
/config/routes.js
module.exports.routes = {
'/': {
view: 'homepage'
}
};
This is by design; you're using the "add person to room" blueprint, so it's assumed you care about the state of the room. If you just want the person returned, use the "create person" blueprint, and supply the room ID in the payload:
POST /person
{
name: "John Doe",
room: 5
}
This will still send out pubsub notifications about a person being added to room 5.