I did some research on handling POST request with node.js, so that's working now, kind of.
I also know how to save a new JSON object to a mongoDB collection with mongoose.
I feel like I'm close to the solution but I'm having trouble finding the answer.
The specific problem
Now, I need some help to put these two together.
Handling POST request [ok+-] => converting to JSON object/mongoose object [not okay] => saving it to collection [ok+-]
The controllers code
Here is the code I'm using. The basic features work :
handling the fields posted and "saving" them to an object
rendering the fields posted (the "fields" object)
The code:
// controller function for adding a ressource
var post = function(req, res){
// incoming form
var form = new formidable.IncomingForm();
var fields = {};
console.dir('ceci est bien un post');
// everytime an field is parsed...
// This is the place where I should do validation and throw errors if needed
form.on('field', function (field, value) {
console.log(field);
console.log(value);
fields[field] = value;
});
// Once the form is parsed completely
form.on('end', function () {
// testing the output
var ressources = util.inspect(fields, {showHidden: false, depth: null});
console.dir(util.inspect(fields, {showHidden: false, depth: null}));
// here save it in mongodb collection
var ressource = new Ressource(fields);
// ressource.save(function (err, ressource, isSuccess) {
// if (err) {
// res.status(400);
// res.send('Error occured ' + err);
// }
// else if (isSuccess === 1) {
// res.status(201);
// res.send(ressource);
// }
// else {
// res.status(400);
// }
// });
// rendering the page with the results
res.render('addRessourceView', {
title: 'Ressources',
fields: ressources
});
});
form.parse(req);
};
And here's the mongoose model:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var ressourceModel = new Schema({
title: { type: String },
descr_short: { type: String },
m_visual: { type: Boolean, default: false },
m_writing:{ type: Boolean, default: false },
m_moving: { type: Boolean, default: false },
m_speaking:{ type: Boolean, default: false },
m_music:{ type: Boolean, default: false },
age: String,
minAge: { type: Number },
maxAge: { type: Number },
prerequisite:{ type: Array,
items: { type: String },
uniqueItems: true},
language: { type: String },
source: { type: String },
intra_identify: { type: Boolean, default: false },
intra_expression: { type: Boolean, default: false },
intra_comprehension: { type: Boolean, default: false },
intra_regulation: { type: Boolean, default: false },
intra_utilisation: { type: Boolean, default: false },
theme_impulsivity: { type: Boolean, default: false },
theme_violence: { type: Boolean, default: false },
inter_identify: { type: Boolean, default: false },
inter_expression: { type: Boolean, default: false },
inter_comprehension: { type: Boolean, default: false },
inter_regulation: { type: Boolean, default: false },
inter_utilisation: { type: Boolean, default: false },
details:{
type: Object,
properties: {
goals: { type: Array,
items: { type: String },
uniqueItems: true},
preparation: String,
exercices: { type: Array,
items: { type: Object }},
reflexion: String
}
},
upload:{ type: Array,
items: { type: Object }}
});
module.exports = mongoose.model('Ressource', ressourceModel);
Note : evrything that is "boolean" is a checkbox input
So all these fields are also in the form (I've just not implemented the file upload).
So basically what I need help for is:
adapting the post fields into something I can use with mongoose and save it
maybe some help on how add some validation (like being sure the minAge is smaller than maxAge, or other stuff)
and if you have time or would want to, how to handle file uploads
I'm really here to learn, I'm just a beginner and I tried to be as clear as possible with my question, and I hope you guys will be able to help me out!
Thanks,
Xogno
Yesterday, I spent the whole day working on it and I came to make it work as I wanted ! Actually, after researching a bit more, it wasn't really difficult : it just needed some good logic and taking the time to do it.
Disclaimer : I'm using 'formidable' package to handle the post request, and learned using it with this tutorial.
I first started by adding the structure with comments, just to write down the logical thinking behind my code. And then I started to implement the code for each field one by one. form.on('field'....){} executes each time a field is parsed, so that's how I cand handle them one by one. To do that i used a switch(true) with multiples cases ... : ... break; .
I added the verification needed, adding an error to the object, and this way all the fields were added one by one to an JSON object.
Regarding errors, I displayed them using EJS template in the html page.
Then, once everything was working (handling POST fields and handling errors), the only thing I had left to do was linking it (saving it) to the mongodb collection, using .save method from mongoose.
So this happens all on server side, and doesn't happen dynamically. Maybe later I will recode it to work dynamically, with ajax or some other framework (Vue.js ?), so it doesn't need to reload the whole page and will be more efficient.
Here's the code in case it might help some!
The code handling the post request
form.on('field') // handling each field one by one
form.on('field', function (field, value) {
console.log(field);
console.log(value);
switch (true) {
//check if title is not empty, if empty, throw error
case field === 'title':
if(value == ''){
// then error
console.log('titre vide')
fields.error = ['title-empty'];
fields[field] = value;
}
else{
fields.error = [];
fields[field] = value;
}
break;
// check if description is not empty
case field === 'short_descr':
if(value == ''){
// then error
console.log('titre vide')
fields.error = ['descr-empty'];
fields[field] = value;
}
else{
fields[field] = value;
}
break;
// when receiving minAge or maxAge, save it as number
case (field === 'minAge' || field === 'maxAge') :
if(field === 'maxAge'){
var maxAge = value;
if(Number(maxAge) < Number(fields.minAge)){
// then error
console.log('Error : maxAge plus petit que minAge')
fields.error.push('maxAge<minAge');
}
}
fields[field] = Number(value);
break;
// if 'objectives'
case field === 'objectives':
console.log('hey we have an objectif!')
// when receiving an objective, verify if there's already one
if('objectives' in fields.details){
//then there is already an objective object
console.log('adding an objective to the list');
fields.details[field].push(value);
}
// then we create the objective entry in fields.details
else {
fields.details[field] = [value];
console.log('ajouté aux détails');
}
break;
// if 'materials'
case field === 'materials':
console.log('hey we have an material!')
// when receiving an objective, verify if there's already one
if('materials' in fields.details){
//then there is already an objective object
console.log('adding an material to the list');
fields.details[field].push(value);
}
// then we create the objective entry in fields.details
else {
fields.details[field] = [value];
console.log('ajouté aux détails');
}
break;
// when receiving an exercice, verify if there's already an exercice that exists
// if there's already an exercice that exists, and this one isn't null, add this one as another exercice
// verify also if ex_time is a number
// and insert it under 'details.exercices' (array of objects : {ex_descr: '', ex_time:''})
// renvoyer une erreur si le temps est vide ou si pas un nombre
case field === 'exercices':
console.log('hey we have an exercice!')
// when receiving an objective, verify if there's already one
//first check if it's not empty
if('exercices' in fields.details){
if(value === ''){
//it's empty, add an error
fields.error.push('Empty-' + field + '[' + j + ']');
console.log('exercice is empty');
}
//then there is already an objective object
console.log('adding an exercice to the list');
var object_exercices = {};
object_exercices.ex = value;
fields.details.exercices.push(object_exercices);
j++;
}
// then we create the objective entry in fields.details
else {
if(value === ''){
//it's empty, add an error
fields.error.push('Empty-' + field + '[' + j + ']');
console.log('exercice is empty');
}
var object_exercices = {};
object_exercices.ex = value;
fields.details.exercices = [object_exercices];
console.log('ajouté aux détails');
j++;
}
break;
//if it's an exercice_time we need to add it to the exercice object in the exercice array
case field === 'exercices-time':
console.log('hey we have an exercice-time!')
// when receiving an exercice-time, verify if there's already an exercice existing
if ('exercices' in fields.details){
if(isNaN(value) || value === ''){
// if it's not a number, push a new error
fields.error.push('NaNexercice_time[' + i + ']');
console.log('but its not a number or is empty ' + i);
//recuparate the object of the exercice
var object_exercices = {};
object_exercices = fields.details.exercices[i];
object_exercices.exercice_time = value;
// add the value of ex_time to it
fields.details.exercices[i] = object_exercices;
i++;
} else {
// if it's a number
// and then there is already an exercice object
console.log('adding an exercice-time to the last exercice ' + i);
//recuparate the object of the exercice
var object_exercices = {};
object_exercices = fields.details.exercices[i];
object_exercices.exercice_time = Number(value);
// add the value of ex_time to it
fields.details.exercices[i] = object_exercices;
i++;
}
}
break;
// if "preparation" add it to details.preparation
case field === 'preparation':
fields.details[field] = value;
console.log('ajouté aux détails');
break;
case field === 'intra_identification' || field === 'intra_regulation' || field === 'intra_understanding'
|| field === 'intra_utilisation' || field === 'intra_expression' || field === 'inter_identification'
|| field === 'inter_expression' || field === 'inter_utilisation' || field === 'inter_regulation'
|| field === 'inter_understanding':
//to see if they've at least checked one competence
fields[field] = value;
break;
// otherwise it's a basic input
default:
fields[field] = value;
}
});
form.on('end') // once the whole form is parsed
// Once the form is parsed completely
form.on('end', function () {
// last error handling
//test if they've at least added one competence
if(!fields.intra_identification && !fields.intra_regulation
&& !fields.intra_understanding && !fields.intra_utilisation
&& !fields.intra_expression && !fields.inter_identification
&& !fields.inter_expression && !fields.inter_utilisation
&& !fields.inter_regulation && !fields.inter_understanding) {
fields.error.push('No competence selected');
console.log('No competence selected');
}
// testing the output
console.dir(util.inspect(fields, {showHidden: false, depth: null}));
//time to save
// see if there were errors
if(fields.error.length < 1){
//no errors
//then save it in the database
console.log('no errors');
// save it in mongodb collection
var ressource = new Ressource(fields);
ressource.save(function (err, ressource, isSuccess) {
if (err) {
res.status(400);
//res.send('Error occured ' + err);
res.render('addRessourceView', {
title: 'Ressources',
fields: fields,
saved: false
});
}
else if (isSuccess === 1) {
res.status(201);
//res.send(ressource);
// render the page with save success
res.render('addRessourceView', {
title: 'Ressources',
fields: fields,
saved: true
});
}
else {
res.status(400);
res.render('addRessourceView', {
title: 'Ressources',
fields: fields,
saved: false
});
}
});
} else {
console.dir(fields.error);
// render the page with save success
res.render('addRessourceView', {
title: 'Ressources',
fields: fields,
saved: false
});
}
});
form.parse(req);
Related
I'm having a problem in pushing these data to an nested object.
Here is what I put on postman as JSON format:
"productId":"621256596fc0c0ef66bc99ca",
"quantity":"10"
here is my code on my controller
module.exports.createOrder = async (data) => {
let product = data.productId;
let oQuantity = data.quantity
let totAmount = data.totalAmount
return User.findById(product).then(user =>{
user.userOrders.products.push({productId:product})
user.userOrders.products.push({quantity:oQuantity})
if (user) {
return user.save().then((savedOrder,err)=>{
if (savedOrder) {
return user
} else {
return 'Failed to create order. Please try again'
}
})
} else {
return "try again"
}
})
}
my schema is:
userOrders:[
{
products:[
{
productName:{
type: String,
required: [true, "UserId is required"]
},
quantity:{
type: Number,
required: [true, "Quantity is required"]
}
}
],
totalAmount:{
type: Number,
required: [true, "Quantity is required"]
},
PurchasedOn:{
type: Number,
default: new Date()
}
}
]
})
i got these error on my CLI
user.userOrders.products.push({productId:product})
^
TypeError: Cannot read properties of null (reading 'userOrders')
Currently i wish to just put these data from postman to the nested object but i think im not using the push use-case well. Any tips?
user is not found and contains null. User.findById(product) returns an empty result. The ID product can't be found in User. First, check if the user was found, then, push the products:
module.exports.createOrder = async (data) => {
let product = data.productId;
let oQuantity = data.quantity
let totAmount = data.totalAmount
return User.findById(product).then(user => {
if (!user) {
return "try again";
}
user.userOrders.products.push({productId:product})
user.userOrders.products.push({quantity:oQuantity})
return user.save().then((savedOrder, err) => {
if (savedOrder) {
return user;
} else {
return 'Failed to create order. Please try again';
}
});
});
};
I am parsing a CSV file and putting the data in a table with AWS DynamoDB.
As it stands right now, I am getting the following error:
One or more parameter values were invalid: An AttributeValue may not contain an empty string
... BEFORE it puts the data in the table. The data is getting to the table, but not before spamming me with that error a million times.
My Code:
var csv = require("fast-csv");
csv.fromPath(file, {
headers: true,
ignoreEmpty: true
})
.on("data", function(data) {
for (var key in data) {
if (data.hasOwnProperty(key)) {
if (data[key] === "" || data[key] === undefined || data[key] === null) {
data[key] = "N/A";
}
}
params = {
TableName: tableName,
Item: {
RefID: {
S: data["Ref-ID"]
},
//lots of other data
}
};
dynamodb.putItem(params, function(err, data) {
if (err) {
console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
}
else {
console.log("Added item:", JSON.stringify(data, null, 2));
}
});
}
})
.on("end", function() {
console.log("done");
});
As you can see, I am converting any possible empty strings to == N/A in an attempt to solve this problem. Any thoughts?
EDIT:
This turns out to be undefined when it should display what it put in the table.
console.log("Added item:", JSON.stringify(data[key], null, 2));
EDIT 2: Changed this code...
dynamodb.putItem(params, function(err, data)
...to this:
dynamodb.putItem(params, function(err, info)
I am still getting the errors, but am now displaying the table correctly.
It appears that dynamoDB at this time does not allow empty strings. I can NOT understand why, but as of this date you cannot not store an attribute of "Key":"".
Please complain to amazon about it. key="" and key=null are very different use cases and are needed.
Try doing field validation on your param.Item Object to verify that everything is set properly; and find the errornous fields that are plaguing your console.
var tableName = "wombat_habitats";
var data = {
"Ref-ID": "Charlie"
};
params = {
TableName: tableName,
Item: {
RefID: {
S: data["Ref-ID"]
},
SomethingElse: {
S: data["Bad-Key"]
}
//lots of other data
}
};
for(var itemKey in params.Item) {
for(var itemAttr in params.Item[itemKey]) {
var value = params.Item[itemKey][itemAttr];
if(value === undefined || value === "") {
console.log("item", itemKey, "of type", itemAttr, "is undefined!")
}
}
}
having an issue using crypto. i'm pretty sure my issue is on line 75 in the saveAccounts function. I believe at this point the accounts var will consist of an empty [] as assigned in the getAccounts function (since there is nothing to 'getItemSync' from 'accounts' in 'storage'. I have a feeling the problem has something to do with the format of the data i'm trying to encrypt but i can't wrap my head around it. been trouble shooting for some time now. days. full-error report at below my code.
console.log('starting password manager');
var crypto = require("crypto-js");
var storage = require('node-persist');
storage.initSync();
var argv = require('yargs')
.command('create', 'Create a new account', function(yargs){
yargs.options({
name: {
demand: true,
alias: 'n',
description: "Account name (eg: Github, SnapChat).",
type: 'string'
},
username: {
demand: true,
alias: 'u',
description: "Please provide a username for your account.",
type: 'string'
},
password: {
demand: true,
alias: 'p',
description: "Please provide a password for your account.",
type: 'string'
},
masterPassword: {
demand: true,
alias: 'm',
description: "Please provide a MASTER password.",
type: 'string'
}
}).help('help');
})
.command('get', 'Get an existing account', function(yargs){
yargs.options({
name: {
demand: true,
alias: 'n',
description: "Account name (eg: Github, SnapChat).",
type: 'string'
},
masterPassword: {
demand: true,
alias: 'm',
description: "Please provide a MASTER password.",
type: 'string'
}
}).help('help');
})
.help('help')
.argv;
var command = argv._[0];
function getAccounts (masterPassword){
console.log("function getAccounts has been run");
var encryptedAccount = storage.getItemSync('accounts');
var accounts = [];
// decrypt
if (typeof encryptedAccount !== 'undefined') {
var bytes = crypto.AES.decrypt(encryptedAccount, masterPassword);
accounts = JSON.parse(bytes.toString(crypto.enc.Utf8));
}
// return accoutns array
return accounts;
}
function saveAccounts (accounts, masterPassword){
console.log("function saveAccounts has been run");
// encrypt accounts
var encryptedAccounts = crypto.AES.encrypt(JSON.stringify(accounts), masterPassword);
// setItemSync
storage.setItemSync('accounts', encryptedAccounts.toString());
// return accounts
return accounts;
}
function createAccount(account, masterPassword){
var accounts = getAccounts(masterPassword);
accounts.push(account);
console.log("back to function createAccount");
saveAccounts(account, masterPassword);
}
function getAccount(accountName, masterPassword){
var accounts = getAccounts(masterPassword);
var matchedAccount;
accounts.forEach(function(account){
if (account.name === accountName) {
matchedAccount = account;
}
});
return matchedAccount;
}
if (command === "create") {
var createdAccount = createAccount({
name: argv.name,
username: argv.username,
password: argv.password
}, argv.masterPassword);
console.log('Account created!');
console.log(createdAccount);
} else if (command === "get") {
if (masterPassword !== argv.m || typeof masterPassword === undefined) {
console.log("Your password was incorrect.");
} else {
var accountReturned = getAccount(argv.name, argv.masterPassword);
if(typeof(accountReturned) === undefined){
console.log("This account doesn't exist.");
} else {
console.log("Your account info:");
console.log(accountReturned);
}
}
}
the full error code looks like this
starting password manager
function getAccounts has been run
back to function createAccount
function saveAccounts has been run
/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/aes.js:96
var keyWords = key.words;
^
TypeError: Cannot read property 'words' of undefined
at Object.C_algo.AES.BlockCipher.extend._doReset (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/aes.js:96:32)
at Object.CryptoJS.lib.Cipher.C_lib.Cipher.BufferedBlockAlgorithm.extend.reset (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/cipher-core.js:119:19)
at Object.CryptoJS.lib.Cipher.C_lib.BlockCipher.Cipher.extend.reset (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/cipher-core.js:457:27)
at Object.CryptoJS.lib.Cipher.C_lib.Cipher.BufferedBlockAlgorithm.extend.init (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/cipher-core.js:104:19)
at Object.subtype.init (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/core.js:68:46)
at Object.subtype.init (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/core.js:68:46)
at Object.subtype.init (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/core.js:68:46)
at Object.C_lib.Base.create (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/core.js:95:32)
at Object.CryptoJS.lib.Cipher.C_lib.Cipher.BufferedBlockAlgorithm.extend.createEncryptor (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/cipher-core.js:63:26)
at Object.CryptoJS.lib.Cipher.C_lib.SerializableCipher.Base.extend.encrypt (/media/david/08053ee9-7733-4986-97be-f5cac7a80746/david/Projects/Node-Password-Manager/node_modules/crypto-js/cipher-core.js:669:37)
You should compare typeof to the string — 'undefined'. See typeof docs
else if (command === "get") {
if (masterPassword !== argv.m || typeof masterPassword === 'undefined') {
console.log("Your password was incorrect.");
} else {
var accountReturned = getAccount(argv.name, argv.masterPassword);
if(typeof(accountReturned) === 'undefined'){
console.log("This account doesn't exist.");
} else {
console.log("Your account info:");
console.log(accountReturned);
}
}
I am working on the same Udemy course I think. Good luck!
I had the same problem, and after trying different things, I found the error was the type of object to encrypt. ---> $crypto.encrypt("string","string"). Simple as that. When you try to decrypt with an object instead of string, you get that error.
If you have long pass_sec on your .env file then change key.
Like this:
this false - > PASS_SEC=thisisverylongstring
this true - > PASS_SEC=short
in the password saving in .env support to be String ...
ecample
SECRET="yoursecret" with quotation mark.
I have a model "User" that has a Many-to-One relationship with a "Subject".
User.js
attributes: {
subject: { model: 'subject' },
}
Subject.js
attributes: {
name: { type: 'string', unique: true, required: true },
}
When I call the blueprint create function for a User "/user" and pass in the data:
{
"name":"Test",
"subject":{"name":"Do Not Allow"}
}
It creates the user and also creates the Subject. However I do not want to allow the subject to be created, I only want to be able to attach an existing one. For example I would like it to reject the subject being created using the above data but allow the subject to be attached by using the below data.
{
"name":"Test",
"subject":1
}
I tried adding a policy (shown below) but this only stops the subject from being created using the URL "/subject" and not the nested create shown above.
'SubjectController':{
'create':false
}
Edit
To help understand what is going on here this is the lifecycle process it is going through:
Before Validation of Subject
After Validation of Subject
Before Creating Subject
After Creating Subject
Before Validation of User
After Validation of User
Before Creating User
Before Validation of User
After Validation of User
After Creating User
As you can see it is validating and creating the subject before it even gets to validating or creating the user.
You want to avoid the creation of an associated object when calling the blueprint creation route.
Create a policy (I've named it checkSubjectAndHydrate) and add it into the policies.js file:
// checkSubjectAndHydrate.js
module.exports = function (req, res, next) {
// We can create a user without a subject
if (_.isUndefined(req.body.subject)) {
return next();
}
// Check that the subject exists
Subject
.findOne(req.body.subject)
.exec(function (err, subject) {
if (err) return next(err);
// The subject does not exist, send an error message
if (!subject) return res.forbidden('You are not allowed to do that');
// The subject does exist, replace the body param with its id
req.body.subject = subject.id;
return next();
});
};
// policies.js
module.exports.policies = {
UserController: {
create: 'checkSubjectAndHydrate',
update: 'checkSubjectAndHydrate',
}
};
You should be passing the subject id (e.g. 1) instead of an object (e.g. { name: 'Hello, World!' }) containing the name of the subject as it's not necessarily unique.
If it is unique, you should replace the object by its id inside a beforeValidate for example.
// User.js
module.exports = {
...
beforeValidate: function (users, callback) {
// users = [{
// "name":"Test",
// "subject":{"name":"Do Not Allow"}
// }]
async.each(users, function replaceSubject(user, next) {
var where = {};
if (_.isObject(user.subject) && _.isString(user.subject.name)) {
where.name = user.subject.name;
} else if(_.isInteger(user.subject)) {
where.id = user.subject;
} else {
return next();
}
// Check the existence of the subject
Subject
.findOne(where)
.exec(function (err, subject) {
if (err) return next(err);
// Create a user without a subject if it does not exist
user.subject = subject? subject.id : null;
next();
});
}, callback);
// users = [{
// "name":"Test",
// "subject":1
// }]
}
};
You can create custom type for subject, and add your logic inside model. I'm not 100% sure I understood the attach sometimes part but maybe this could help:
models/User.js
module.exports = {
schema: true,
attributes: {
name: {
type: 'string'
},
subject: {
type: 'json',
myValidation: true
}
},
types: {
myValidation: function(value) {
// add here any kind of logic...
// for example... reject if someone passed name key
return !value.name;
}
}
};
You can find more info here http://sailsjs.org/documentation/concepts/models-and-orm/validations at the bottom of the page.
If I totally missed the point... The second option would be to add beforeCreate and beforeUpdate lifecycle callback to your model like this:
models/User.js
module.exports = {
schema: true,
attributes: {
name: {
type: 'string'
},
subject: {
type: 'json'
}
},
beforeCreate: function (values, cb) {
// for example... reject creating of subject if anything else then value of 1
if (values.subject && values.subject !== 1) return cb('make error obj...');
cb();
},
beforeUpdate: function (values, cb) {
// here you can add any kind of logic to check existing user or current update values that are going to be updated
// and allow it or not
return cb();
}
};
By using this you can use one logic for creating and another one for updating... etc...
You can find more info here: http://sailsjs.org/documentation/concepts/models-and-orm/lifecycle-callbacks
EDIT
Realized you have trouble with relation, and in above examples I thought you are handling type json...
module.exports = {
schema: true,
attributes: {
name: {
type: 'string'
},
subject: {
model: 'subject'
}
},
beforeValidate: function (values, cb) {
// subject is not sent at all, so we just go to next lifecycle
if (!values.subject) return cb();
// before we update or create... we will check if subject by id exists...
Subject.findOne(values.subject).exec(function (err, subject) {
// subject is not existing, return an error
if (err || !subject) return cb(err || 'no subject');
//
// you can also remove subject key instead of sending error like this:
// delete values.subject;
//
// subject is existing... continue with update
cb();
});
}
};
I am using the latest version of JTable from http://jtable.org/ (downloaded it yesterday). I setup my JTable as shown below (I also included the server-side code below, which is written in C#). The List function works (the data shows up in the table), the Add function works, and the Delete function works. However, when I go to Edit a row, there is an error when trying to populate the data for the "ElevationsMulti" field. I get an error that simply says, "Cannot load options for field ElevationsMulti."
JTable Code:
$('#ReportsContainer').jtable({
title: 'Reports',
actions: {
listAction: '/Report_SingleEstimate/GetReportNames?customerId=' + customerId,
createAction: '/Report_SingleEstimate/AddReport',
updateAction: '/Report_SingleEstimate/EditReport',
deleteAction: '/Report_SingleEstimate/DeleteReport'
},
fields: {
ReportID: {
key: true,
list: false
},
ReportName: {
title: 'Report Name'
},
CustomerID: {
title: 'Customer',
list: false,
options: '/Estimates/GetCustomers',
defaultValue: customerId
},
PlanNameID: {
title: 'Plan Name',
dependsOn: 'CustomerID',
options: function (data) {
if (data.source == 'list') {
return '/Estimates/GetListOfPlanNames?customerId=0';
}
//data.source == 'edit' || data.source == 'create'
return '/Estimates/GetListOfPlanNames?customerId=' + data.dependedValues.CustomerID;
}
},
ProductID: {
title: 'Product',
options: '/Estimates/GetProducts'
},
HeaderFieldsMulti: {
title: 'Fields',
options: '/Report_SingleEstimate/GetHeaderFields',
type: 'multiselectddl',
list: false
},
ElevationsMulti: {
title: 'Elevations',
type: 'multiselectddl',
dependsOn: ['PlanNameID', 'ProductID'],
options: function (data) {
if (data.source == 'list') {
return '/Elevation/GetAllElevations';
}
return '/Report_SingleEstimate/GetElevations?PlanNameID=' + data.dependedValues.PlanNameID +
'&ProductID=' + data.dependedValues.ProductID;
},
list: false
}
}
});
$('#ReportsContainer').jtable('load');
Not sure if it makes a difference in JTable, but the ElevationsMulti depends on the PlanNameID and ProductID fields, and the PlanNameID field depends on the CustomerID fields. In other words, the ElevationsMulti field depends on a field that depends on another field (multiple nested dropdowns).
C# server-side code:
[HttpPost]
public JsonResult GetElevations(int PlanNameID, int ProductID)
{
try
{
int estimateId = Estimates.getEstimateId(PlanNameID, ProductID);
List<MyDropdownList> elevations = Estimate_ElevationList.listElevationsByEstimateForDropdown(estimateId);
return Json(new { Result = "OK", Options = elevations });
}
catch (Exception ex)
{
return Json(new { Result = "ERROR", Message = ex.Message });
}
}
Error here:
Further debugging has given me a more specific error.
The parameters dictionary contains a null entry for parameter 'PlanNameID' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.JsonResult GetElevations(Int32, Int32)' in 's84.Controllers.Report_SingleEstimateController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Basically, JTable sends PlanNameID to the server as a null value. Which seems to indicate that JTable has not loaded the options for the PlanNameID field yet when it makes the server call for the ElevationsMulti field.
How do I make JTable wait to load the ElevationsMulti field until after the PlanNameID field has been loaded?
Problem solved.
The problem came from using the Type "multiselectddl" in JTable. I changed the code in JTable that creates the multiselectddl to the same code as the regular dropdown. Here is the code:
_createInputForRecordField: function (funcParams) {
...
//Create input according to field type
if (field.type == 'date') {
return this._createDateInputForField(field, fieldName, value);
} else if (field.type == 'textarea') {
return this._createTextAreaForField(field, fieldName, value);
} else if (field.type == 'password') {
return this._createPasswordInputForField(field, fieldName, value);
} else if (field.type == 'checkbox') {
return this._createCheckboxForField(field, fieldName, value);
} else if (field.options) {
if (field.type == 'multiselectddl') {
return this._createDropDownListMultiForField(field, fieldName, value, record, formType, form);
} else if (field.type == 'radiobutton') {
return this._createRadioButtonListForField(field, fieldName, value, record, formType);
} else {
return this._createDropDownListForField(field, fieldName, value, record, formType, form);
}
} else {
return this._createTextInputForField(field, fieldName, value);
}
},
_createDropDownListMultiForField: function (field, fieldName, value, record, source, form) {
//Create a container div
var $containerDiv = $('<div class="jtable-input jtable-multi-dropdown-input"></div>');
//Create multi-select element
var $select = $('<select multiple="multiple" class="' + field.inputClass + '" id="Edit-' + fieldName + '" name=' + fieldName + '></select>')
.appendTo($containerDiv);
var options = this._getOptionsForField(fieldName, {
record: record,
source: source,
form: form,
dependedValues: this._createDependedValuesUsingForm(form, field.dependsOn)
});
this._fillDropDownListWithOptions($select, options, value);
return $containerDiv;
}