Create unique multikey index via model settings - sails.js

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.

Related

Mongoose Schema planning: using ObjectID reference or using array of type: [otherSchema]?

I'm currently planning out the database structure for an app I'm building, and something in this linked answer raised some questions for me.
In the structure that Shivam proposed, he sometimes references another collection directly, in other words within one Schema he defines the type of a field to be an array of another schema type. Example:
import { Schema } from "mongoose";
import { QuestionSchema } from "./question-schema";
const mongoose = require('mongoose');
export const QuestionSetSchema: Schema = new Schema({
questionSet: {
type: [QuestionSchema],
validate: {
validator: function(value: any) {
return value.length === 12;
},
message: 'Question set must be 12.'
}
},
}, {
timestamps: true
});
In other cases, he only uses an ObjectID reference to another schema/collection:
export const CandidateSchema: Schema = new Schema({
name: String,
email: String, // you can store other candidate related information here.
totalAttempt: {
type: Number,
default: 0,
validate: {
validator: function(value: number) {
return value === 3;
},
message: 'You have already done three attempts.'
}
},
candidateQuestionAnswers: {
type: [Schema.Types.ObjectId],
ref: 'CandidateQuesAnswer'
}
}, {
timestamps: true
});
What are the use cases for each of the above? Does the "type:[otherSchema]" method actually embed instances of that collection or does it only provide their properties to the Schema they are called from?

Relationship field within same collection?

I have a keystonejs model for product categories (MongoDB). Some categories should have subcategories. Currently I have set a relationship field "ChildCategoryOf", where I can manually select the Parent Category in admin panel. To have more functionality, I would like to create another Field called "ParentCategoryOf" that would consist an Array of subcategories. How is it possible to have a field that automatically stores Child categories in Array? I imagine it like this:
Current model:
let ProductCategory = new keystone.List('ProductCategory', {
autokey: {
from: 'name',
path: 'key',
unique: true
}
});
ProductCategory.add({
name: {
type: String,
required: true
},
ChildCategoryOf: {
type: Types.Relationship,
ref: 'ProductCategory',
many: false,
required: false,
},
IsParentCategory: Types.Boolean,
});
For categories in mongo you can use an inheritance like model, in which you can store the parentId and ancestor Id for each model and its children and then add a method to the mongoose model to add a child each time you call it on an instance, this would be like below code:
const ProductCategory = new Schema({
name: String,
parent: { type: Schema.Types.ObjectId, ref: 'ProductCategory'},
ancestors: [{type: Schema.Types.ObjectId, ref: 'ProductCategory'}],
children: [{type: Schema.Types.ObjectId, ref: 'ProductCategory'}]
});
ProductCategory.methods = {
addChild: function(child){
let that = this;
child.parent = this._id;
child.ancestors = this.ancestors.concat([this._id]);
return this.model('ProductCategory').create(child).addCallback
(function(child){
that.children.push(child._id);
that.save();
});
}
};
then later from where you want to find products by their categories you should concat the category id that you find with it's children ids and use this array in find query with $in operator to find all the products in a category with its children.

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.

Remove undesirable fields from being returned in sails

I am new to SailsJS and stuck in Data Model as follows:
I have a User model as follows:
module.exports = {
attributes: {
firstName: {
type: 'string'
},
email: {
type: 'email',
required: true
},
password: {
type: 'String'
},
passwordSalt: {
type: 'String'
},
projects:{
collection: 'ProjectMember',
via: 'userId'
}
}
};
Task Model :
module.exports = {
taskName: {
type: 'String'
},
userId: {
model: 'User'
}
};
In Task model, it is getting all fields from User table which is not required while task data is rendered. I was planning to create one more model called TinyUser which stores only required fields to be shown when task data is rendered.
But TinyUser should just refer User table and get required data from it rather than we creating all data for TinyUser manually when user data is created.
Is there any way this can be achieved in Sails?
Thanks in Advance..
I'm not sure about your question, but this will return a list of required attributes for any model
_.find(sails.models.<YOUR MODEL>._attributes, function(attr){return attr.required})
If your intent it to simply remove undesirable fields you can override the toJSON / toObject methods
see
https://github.com/balderdashy/waterline-docs/blob/master/models.md#toobjecttojson-instance-methods
User.find({select:['firstName', 'email']}).exec()