SailsJS Blueprints going haywire - mysql

I had been working on an app using SailsJS v0.12. The app was working as expected. But, for some modules, it made sense to upgrade to v1.0 because it offered some great features, and the website implied (or seemed so) that the upgrade process was simple.
That wasn't the case. After a day of tedious refactoring, I succeeded in building the existing code and started working on the new modules. The problem to focus on began here.
I ran the functions as I did previously in POSTman and I stopped getting response. To check individual models, I tried using the Blueprints. Since no structural changes were made to any of the models, this should've worked. But I couldn't get any response in POST; GET was still working. I logged off for a break and then made a GET request to a model with some associations (working perfectly before).
I got this response
UsageError: Invalid populate(s). Details: Could not populate
customer because of ambiguous usage. This is a singular ("model")
association, which means it never refers to more than one associated
record. So passing in subcriteria (i.e. as the second argument to
.populate()) is not supported for this association, since it
generally wouldn't make any sense. But that's the trouble-- it looks
like some sort of a subcriteria (or something) was provided! (Note
that subcriterias consisting ONLY of omit or select are a special
case that does make sense. This usage will be supported in a future
version of Waterline.)
Here's what was passed in: { limit: 30 }
The complete JSON response is:
{
"stack": "UsageError: Invalid populate(s).\nDetails:\n Could not populate `customer` because of ambiguous usage. This is a singular (\"model\") association, which means it never refers to more than _one_ associated record. So passing in subcriteria (i.e. as the second argument to `.populate()`) is not supported for this association, since it generally wouldn't make any sense. But that's the trouble-- it looks like some sort of a subcriteria (or something) _was_ provided!\n(Note that subcriterias consisting ONLY of `omit` or `select` are a special case that _does_ make sense. This usage will be supported in a future version of Waterline.)\n\nHere's what was passed in:\n{ limit: 30 }\n\n at findRecords (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\hooks\\blueprints\\actions\\find.js:40:21)\n at routeTargetFnWrapper (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\router\\bind.js:181:5)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:164:37)\n at param (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:138:11)\n at pass (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:145:5)\n at nextRoute (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:100:7)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:167:11)\n at alwaysAllow (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\hooks\\policies\\index.js:224:11)\n at routeTargetFnWrapper (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\router\\bind.js:181:5)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:164:37)\n at param (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:138:11)\n at pass (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:145:5)\n at nextRoute (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:100:7)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:167:11)\n at module.exports (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\hooks\\cors\\clear-headers.js:14:3)\n at routeTargetFnWrapper (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\router\\bind.js:181:5)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:164:37)\n at param (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:138:11)\n at pass (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:145:5)\n at nextRoute (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:100:7)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:167:11)\n at sails.router.bind._middlewareType (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\hooks\\csrf\\index.js:148:11)\n at routeTargetFnWrapper (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\router\\bind.js:181:5)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:164:37)\n at param (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:138:11)\n at pass (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:145:5)\n at nextRoute (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:100:7)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:167:11)\n at _addResViewMethod (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\hooks\\views\\res.view.js:372:3)\n at routeTargetFnWrapper (D:\\Dev\\NodeJS\\zingr\\node_modules\\sails\\lib\\router\\bind.js:181:5)\n at callbacks (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:164:37)\n at param (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:138:11)\n at pass (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:145:5)\n at nextRoute (D:\\Dev\\NodeJS\\zingr\\node_modules\\express\\lib\\router\\index.js:100:7)",
"message": "Invalid populate(s).\nDetails:\n Could not populate `customer` because of ambiguous usage. This is a singular (\"model\") association, which means it never refers to more than _one_ associated record. So passing in subcriteria (i.e. as the second argument to `.populate()`) is not supported for this association, since it generally wouldn't make any sense. But that's the trouble-- it looks like some sort of a subcriteria (or something) _was_ provided!\n(Note that subcriterias consisting ONLY of `omit` or `select` are a special case that _does_ make sense. This usage will be supported in a future version of Waterline.)\n\nHere's what was passed in:\n{ limit: 30 }\n",
"name": "UsageError",
"code": "E_INVALID_POPULATES",
"details": "Could not populate `customer` because of ambiguous usage. This is a singular (\"model\") association, which means it never refers to more than _one_ associated record. So passing in subcriteria (i.e. as the second argument to `.populate()`) is not supported for this association, since it generally wouldn't make any sense. But that's the trouble-- it looks like some sort of a subcriteria (or something) _was_ provided!\n(Note that subcriterias consisting ONLY of `omit` or `select` are a special case that _does_ make sense. This usage will be supported in a future version of Waterline.)\n\nHere's what was passed in:\n{ limit: 30 }"
}
The confusing aspect is that this is only happening locally. The blueprints work perfectly as expected on the deployment (same code, double-checked) hosted on Zeit.co. I have tested this enough times to deduce that this isn't a fault in my models. Nonetheless, here are the models - Bill and Customer:
models\Bill.js
module.exports = {
primaryKey:'id',
attributes: {
createdAt: { type: 'number', autoCreatedAt: true, },
updatedAt: { type: 'number', autoUpdatedAt: true, },
id:{
type:'string',
required:true
},
bill_number:{
type:'string'
},
admin_id:{
type:'string'
},
channel:{
type:'string'
},
amount:{
type:'number'
},
discount:{
type:'number',
defaultsTo:0
},
order_date:{
type: 'number'
},
status:{
type:'string',
isIn:['generated', 'settled', 'canceled'],
defaultsTo:'generated'
},
type:{
type:'string',
isIn:['dine-in', 'take-away', 'delivery', 'logistics-delivery'],
required:true
},
logisticsPartner:{
type:'string',
defaultsTo:'phone'
},
//association
customer:{
model:'customer'
},
store:{
model:'store'
},
orderContents:{
collection:'billmapping',
via:'bill'
},
transactions:{
collection:'transaction',
via:'bill'
}
}
};
models\Customer.js
module.exports = {
attributes: {
createdAt: { type: 'number', autoCreatedAt: true, },
updatedAt: { type: 'number', autoUpdatedAt: true, },
id: { type: 'number', autoIncrement: true}, // <-- for SQL databases
name:{
type:'string'
},
number:{
type:'string',
unique:true
},
email:{
type:'string',
unique:true
},
age:{
type:'number'
}
}
};
This issue has had me pulling my hair out for 8 hours now. Can anybody shed some light or share some suggestions?
Thanks in advance.

Related

Sequelize - Enable paranoid: true on query level while its false on the defined Model

So lets say I have a Sequelize model defined with paranoid defaulting to "false":
const Country = sequelize.define('Country', {
name: {
type: DataTypes.STRING,
defaultValue: '',
},
code: {
type: DataTypes.STRING,
defaultValue: '',
},
currency: {
type: DataTypes.STRING,
defaultValue: '',
},
languages: {
type: DataTypes.STRING,
defaultValue: '',
},
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
deletedAt: DataTypes.DATE
});
Now when I invoke Model.destroy() on any records of Country table, the records would be hard deleted. Enabling paranoid: true on the Model definition would result in soft deletes.
I wanted to achieve the opposite of this. Where, the paranoid flag on model definition is set to false and we need to explicitly define a flag on the Model.destroy() method to soft-delete an entry and by default all records would be hard deleted.
I tried to sift through the documentation in order to find something but couldn't. Would appreciate any help I can get in case I missed something or if there's a workaround.
Why I need to do this? Some background
I joined a project with about 100+ defined models (even more) on which the paranoid flag is not defined and is false by default. Thankfully, the createdAt, updatedAt and deletedAt timestamps are defined explicitly. But any call to the Model.destroy() function results in a hard delete.
I need to introduce the functionality of a soft delete without changing any model definitions (because that would result in unintended consequences). Again, thankfully, the Model.destroy() method is wrapped in a function which is used in the entire codebase.
I was thinking of introducing an optional flag on this wrapper function which would indicate whether the delete needs to be soft or hard. So the default functionality would be hard delete unless explicitly specified to be a soft delete.
Worst case solution I can think of is that in case soft delete is required, then replace the destroy method with a raw query where I update the deletedAt timestamp manually. But hoping to find cleaner solutions than this :)
The simplest solution would be to use force: false option in case of soft-delete and force: true in case of hard-delete:
async function wrappedDestroy(item, isSoftDelete) {
await item.destroy({ force: !isSoftDelete })
}
Of course, you need to turn on paranoid: true in the model because it also affects all findAll/findOne queries as well (I suppose you wish to hide all soft-deleted records from findAll/findOne by default).

Populating multiple tables in sails waterline orm

I am working on a sails applications which contains multiple(>2) tables which I need to join with the help of populate method
e.g.
Category.js model
attributes: {
CategoryID:{
type:"integer",
required:true,
primaryKey:true,
autoIncrement:true
},
SubCategories:{ //REFERING TO SUB-CATEGORY TABLE
collection:'SubCategory',
via:'CategoryID'
},
CategoryName:{
type:"string",
required:true,
maxLength:50
}
}
this is SubCategory.js model.
attributes: {
id:{
type:'integer',
required:true,
primaryKey:true,
autoIncrement:true,
maxLength:10,
columnName:'SubCategoryID'
},
CategoryID:{
model:'Category' //REFERING TO CATEGORY TABLE
},
ProductsOfCategory:{ //REFERING TO PRODUCT TABLE
collection:'Product',
via:'SubCategoryID'
},
SubCategory:{
type:"string",
required:true,
maxLength:50
}
}
and Product.js model
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
maxLength: 10,
columnName:'ProductID'
},
SubCategoryID: {
model:'SubCategory'
},
ProductDetails:{
collection:'ProductDetails',
via:'ProductID'
},
ProductName: {
type: "string",
required: true,
maxLength: 50
}
}
and ProductDeatils.js model
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
maxLength: 10,
columnName:'ProductDetailID'
},
ProductID:{
model:'Product'
},
Size:{
type:"string",
required:true,
maxLength:10
},
Color:{
type:"string",
required:true,
maxLength:10
}
}
On Populating, I am able to populate the category and sub-category of each category.
Category.find()
.populate('SubCategories')
.exec(function(err, categories){
if (err) {
console.log(err);
return res.json(err);
}
console.log(categories);
res.json(categories);
})
How to populate the all above table in one go such that after final query we get all the above details in one json.
We get join of all above tables
that is category having all sub-categories, sub-category having all products and all product have product details in one json
You ask a great question. There has been massive interest in getting nested populate feature into sails, literally tens of issue requests and PRs etc.
Take a look at some history here:
[FEATURE REQUEST] Recursively populate #308 - i was late to the party, making the request on October 29th 2014 as you'll see in the history.
As far as I know, most conversations eventually converged here (after a couple of years of Sails users requesting the feature):
Deep populate #1052 (the issue remains open as of writing 14 Jan 2016)
It is unclear from the state of that Issue where we are. The history of both links does suggest alternative workarounds others have used.
My hunch is that recursive populate is not supported out of the box.
What I did when using waterline model associations with SailsJS, was work with a package like async.js - use something like waterfall to explicitly populate the child relationships programmatically. You can combine doing this with overriding the default toJSON() of the models you invoke to add their relationships (which you have programmatically populated) to the JSON response. You could equally choose to use the built-in promises to achieve the same thing.
Found this (dated, 2014) SOF Question which offers more information.
Someone, do please correct me here if I have missed this feature addition in a recent Sails or Waterline version - couldn't find anything in the release notes for either project to say this was supported.

Sequelize.js: query for object without relations

The Sequelize documentation is very limited when it comes to querying relations:
For example, I would like to query all Projects with no Tasks that have a state OR description defined (therefore, either these projects have no tasks at all, or they only have tasks that have only a state or only a description, but not both).
You might think something like this would work:
Project.findAll({
include: [{
model: Task,
where: {
$or: {
state: null,
description: null
}
}
}]
});
However, this doesn't work because it wouldn't return any Projects that have no tasks at all.

Sequelize include (how to structure query)?

I have a query I'm trying to perform based on a one to many relationship.
As an example there is a model called Users and one called Projects.
Users hasMany Projects
Projects have many types which are stored in a type (enum) column. There are 4 different types that potentially a user may have that I want to load. The catch is I want to include the most recent project record (createdAt column) for all networks that potentially will be there. I have not found a way to structure the query for it to work as an include. I have however found a way to do a raw query which does what I want.
I am looking for a way without having to do a raw query. By doing the raw query I have to map the returned results to users I've returned from the other method, or I have to do a simple include and then trim off all the results that are not the most recent. The latter is fine, but I see this getting slower as a user will have many projects and it will keep growing steadily.
This allow serialize a json for anywhere action about a model. Read it, very well
sequelize-virtual-fields
// define models
var Person = sequelize.define('Person', { name: Sequelize.STRING });
var Task = sequelize.define('Task', {
name: Sequelize.STRING,
nameWithPerson: {
type: Sequelize.VIRTUAL,
get: function() { return this.name + ' (' + this.Person.name + ')' }
attributes: [ 'name' ],
include: [ { model: Person, attributes: [ 'name' ] } ],
order: [ ['name'], [ Person, 'name' ] ]
}
});
// define associations
Task.belongsTo(Person);
Person.hasMany(Task);
// activate virtual fields functionality
sequelize.initVirtualFields();

A Sequelize column that cannot be updated

Is it possible to create a column on a MySQL table using Sequelize that can be initialized when creating a new row, but never updated?
For example, a REST service allows a user to update his profile. He can change any field except his id. I can strip the id from the request on the API route, but that's a little redundant because there are a number of different models that behave similarly. Ideally, I'd like to be able to define a constraint in Sequelize that prevents the id column from being set to anything other than DEFAULT.
Currently, I'm using a setterMethod for the id to manually throw a ValidationError, but this seems hackish, so I was wondering if there's a cleaner way of doing this. Even worse is that this implementation still allows the id to be set when creating a new record, but I don't know a way around this as when Sequelize generates the query it calls setterMethods.id to set the value to DEFAULT.
return sequelize.define('Foo',
{
title: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
notEmpty: true
}
}
},
{
setterMethods: {
id: function (value) {
if (!this.isNewRecord) {
throw new sequelize.ValidationError(null, [
new sequelize.ValidationErrorItem('readonly', 'id may not be set', 'id', value)
]);
}
}
}
}
);
Look at this Sequelize plugin:
https://www.npmjs.com/package/sequelize-noupdate-attributes
It adds support for no update and read-only attributes in Sequelize models.
In your specific case, you could configure the attribute with the following flags:
{
title: {
type: DataTypes.STRING,
allowNull: false,
unique : true,
noUpdate : true
}
}
That will allow the initial set of the title attribute if is null, and then prevent any further modifications once is already set.
Disclaimer: I'm the plugin author.