I am new to both Node.js and the Sails.js framework. I am trying to build a website/application with the framework, but am having trouble getting the model part to work as expected. I have looked at the documentation quite a bit, but still am not able to perform this basic task. I am using the PostgreSQL adapter.
The stack is as follows:
TypeError: Fragrance.create is not a function\n at Object.module.exports.index (/home/kelly/workspace/Hawthorne-Acres/api/controllers/HomeController.js:16:19)\n at wrapper (/usr/local/lib/node_modules/sails/node_modules/#sailshq/lodash/lib/index.js:3250:19)\n at routeTargetFnWrapper (/usr/local/lib/node_modules/sails/lib/router/bind.js:181:5)\n at callbacks (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:164:37)\n at param (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:138:11)\n at param (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:135:11)\n at pass (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:145:5)\n at nextRoute (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:100:7)\n at callbacks (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:167:11)\n at alwaysAllow (/usr/local/lib/node_modules/sails/lib/hooks/policies/index.js:224:11)\n at routeTargetFnWrapper (/usr/local/lib/node_modules/sails/lib/router/bind.js:181:5)\n at callbacks (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:164:37)\n at param (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:138:11)\n at param (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:135:11)\n at pass (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:145:5)\n at nextRoute (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:100:7)\n at callbacks (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:167:11)\n at module.exports (/usr/local/lib/node_modules/sails/lib/hooks/cors/clear-headers.js:14:3)\n at routeTargetFnWrapper (/usr/local/lib/node_modules/sails/lib/router/bind.js:181:5)\n at callbacks (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:164:37)\n at param (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:138:11)\n at pass (/usr/local/lib/node_modules/sails/node_modules/#sailshq/express/lib/router/index.js:145:5)"
Fragrance.js (located in the api/models folder):
module.exports = {
attributes: {
fragranceName: {
type: 'string',
required: true
},
listings: {
collection: "listing",
via: "fragrance"
}
}
};
The controller that calls the function
/**
* HomeController
*
* #description :: Server-side logic for managing Homes
* #help :: See http://sailsjs.org/#!/documentation/concepts/Controllers
*/
var Fragrance = require("../models/Fragrance");
module.exports = {
/**
* `HomeController.Index()`
*/
index: function(req, res) {
Fragrance.create({ fragranceName: 'Vanilla' }).exec(function(err, rec) {
console.log(rec.id);
});
return res.render("index", { title: "Welcome" });
},
/**
* `HomeController.About()`
*/
about: function(req, res) {
console.log("About place");
return res.render("about", { title: "About" });
},
/**
* `HomeController.Contact()`
*/
contact: function(req, res) {
return res.render("contact", { title: "Contact" });
}
};
It is probably something obvious, but I have made an effort to figure it out on my own, just without success. As such, any help would be appreciated.
Thanks.
The models in model folder are globally available. You can disable the functionality in config/globals.js. So you can drop
var Fragrance = require("../models/Fragrance");
Then you can create a record via:
Fragrance.create method
or
sails.models.fragrance.create method (Notice the model name is in lowercase.)
Personally, I prefer the second pattern and turning off the availability of models globally.
Related
I need to retrieve an object and also get the relations and nested relations.
So, I have the three models below:
User model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pets: {
collection: 'pet',
via: 'owner',
}
}
Pet model:
module.exports = {
attributes: {
name: {
type: 'string'
},
owner: {
model: 'user'
},
vaccines: {
collection: 'vaccine',
via: 'pet',
}
}
Vaccine model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pet: {
model: 'pet'
}
}
Calling User.findOne(name: 'everton').populate('pets').exec(....) I get the user and associated Pets. How can I also get the associated vaccines with each pet? I didn't find references about this in the official documentation.
I've ran into this issue as well, and as far as I know, nested association queries are not built into sails yet (as of this post).
You can use promises to handle the nested population for you, but this can get rather hairy if you are populating many levels.
Something like:
User.findOne(name: 'everton')
.populate('pets')
.then(function(user) {
user.pets.forEach(function (pet) {
//load pet's vaccines
});
});
This has been a widely discussed topic on sails.js and there's actually an open pull request that adds the majority of this feature. Check out https://github.com/balderdashy/waterline/pull/1052
While the answer of Kevin Le is correct it can get a little messy, because you're executing async functions inside a loop. Of course it works, but let's say you want to return the user with all pets and vaccines once it's finished - how do you do that?
There are several ways to solve this problem. One is to use the async library which offers a bunch of util functions to work with async code. The library is already included in sails and you can use it globally by default.
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets;
// async.each() will perform a for each loop and execute
// a fallback after the last iteration is finished
async.each(pets, function (pet, cb) {
Vaccine.find({pet: pet.id})
.then(function(vaccines){
// I didn't find a way to reuse the attribute name
pet.connectedVaccines = vaccines;
cb();
})
}, function(){
// this callback will be executed once all vaccines are received
return res.json(user);
});
});
There is an alternative approach solving this issue with bluebird promises, which are also part of sails. It's probably more performant than the previous one, because it fetches all vaccines with just one database request. On the other hand it's harder to read...
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets,
petsIds = [];
// to avoid looping over the async function
// all pet ids get collected...
pets.forEach(function(pet){
petsIds.push(pet.id);
});
// ... to get all vaccines with one db call
var vaccines = Vaccine.find({pet: petsIds})
.then(function(vaccines){
return vaccines;
});
// with bluebird this array...
return [user, vaccines];
})
//... will be passed here as soon as the vaccines are finished loading
.spread(function(user, vaccines){
// for the same output as before the vaccines get attached to
// the according pet object
user.pets.forEach(function(pet){
// as seen above the attribute name can't get used
// to store the data
pet.connectedVaccines = vaccines.filter(function(vaccine){
return vaccine.pet == pet.id;
});
});
// then the user with all nested data can get returned
return res.json(user);
});
I have created a Sails.js Model by using command "sails generate model". The model is generated as follows:
module.exports = {
connection: 'someMysqlServer',
attributes: {
amount : { type: 'float' },
country : { type: 'string' },
month : { type: 'string' }
}
};
I am trying to use .create() function of an instance of this model , inside a controller as follow , but I receive error :
var myModel = require ('../models/Info') ;
module.exports = {
getInfo: function (req, res) {
console.log("inside getInfo controller");
myModel.create({amount:3.5 , country:'spain' , month: 'january' }) ;
} ,
}
I receive the following error while using .create() function :
error: Sending 500 ("Server Error") response:
TypeError: Object #<Object> has no method 'create'
at module.exports.getInfo (/vagrant_data/inputinterface/b2bInputInterface/api/controllers/InputPageController.js:38:26)
at routeTargetFnWrapper (/usr/lib/node_modules/sails/lib/router/bind.js:178:5)
at callbacks (/usr/lib/node_modules/sails/node_modules/express/lib/router/index.js:164:37)
at param (/usr/lib/node_modules/sails/node_modules/express/lib/router/index.js:138:11)
at param (/usr/lib/node_modules/sails/node_modules/express/lib/router/index.js:135:11)
at pass (/usr/lib/node_modules/sails/node_modules/express/lib/router/index.js:145:5)
at nextRoute (/usr/lib/node_modules/sails/node_modules/express/lib/router/index.js:100:7)
at callbacks (/usr/lib/node_modules/sails/node_modules/express/lib/router/index.js:167:11)
at /usr/lib/node_modules/sails/lib/router/bind.js:186:7
at alwaysAllow (/usr/lib/node_modules/sails/lib/hooks/policies/index.js:209:11) [TypeError: Object #<Object> has no method 'create']
According to the following http://sailsjs.org/#/documentation/reference/waterline/models/create.html
this method should be accessible automatically ?
Models are globalized for you by Sails; there is no need to attempt to require() them on your own nor will this work as intended.
If your model file lives in api/models/Info.js, then your controller would look something like:
module.exports = {
getInfo: function (req, res) {
Info.create({amount:3.5 , country:'spain' , month: 'january' });
}
}
Rather than trying to follow the Waterline docs when creating a Sails app, you'll have a lot more luck following the Sails docs for models.
Ok, I have node as backend, it has the following Mongoose model:
var SomeSchema = new Schema({
name: {type: String, required: true},
iplanned: {type: String, default:60}
});
SomeSchema.virtual('planned')
.get(function () {
return parseInt(this.iplanned / 60, 10) + ' mins';
})
.set(function (val) {
this.iplanned = parseInt(val, 10) * 60;
});
someModel = mongoose.model('Some', SomeSchema);
So far it is good, from node.js side of things I can work with the records and access this planned field as I like.
The following is a simple responder to serve this list through http:
exports.list = function (req, res) {
someModel.find(function (err, deeds) {
return res.send(deeds);
});
});
And here's the problem - the virtual field planned is not included in each record (well, it is understandable, sort of). Is there a way to inject somehow my virtual field to each record? Or I have to do the virtual conversion on a front end as well? (Yeah, I know that there's Meteor.js out there, trying go without it here).
In your schema you should redefine toJSON() method:
SomeSchema.methods.toJSON = function () {
var obj = this.toObject();
obj.planned = this.planned;
return obj;
};
I see lots of examples where node.js / express router code is organized like this:
// server.js
var cats = require('cats');
app.get('/cats', cats.findAll);
// routes/cats.js
exports.findAll = function(req, res) {
// Lookup all the cats in Mongoose CatModel.
};
I'm curious if it would be okay to put the logic to create, read, update and delete cats in the mongoose CatModel as methods? So you could do something like cat.findAll(); The model might look something like this:
var Cat = new Schema({
name: {
type: String,
required: true
}
});
Cat.methods.findAll = function(callback) {
// find all cats.
callback(results);
}
Then you could use this in your router:
app.get('/cats', cats.findAll);
If if further logic / abstraction is needed (to process the results) then one could do it in routes/cats.js.
Thanks in Advance.
Obviously your architecture is completely up to you. I've found that separating my routes (which handle business logic) and models (which interact with the db) is necessary and very easy.
So I would usually have something like
app.js
var cats = require ('./routes/cats');
app.get('/api/cats', cats.getCats);
routes/cats.js
var Cats = require ('../lib/Cats');
exports.getCats = function (req, res, next) {
Cat.get (req.query, function (err, cats) {
if (err) return next (err);
return res.send ({
status: "200",
responseType: "array",
response: cats
});
});
};
lib/Cat.js
var catSchema = new Schema({
name: {
type: String,
required: true
}
});
var Cat = mongoose.model ('Cat', catSchema);
module.exports = Cat;
Cat.get = function (params, cb) {
var query = Cat.find (params);
query.exec (function (err, cats) {
if (err) return cb (err);
cb (undefined, cats);
});
};
So this example doesn't exactly show an advantage, but if you had an addCat route, then the route could use a "getCatById" function call, verify the cat doesn't exist, and add it. It also helps with some nesting. The routes could also be used for sanitizing the objects before sending them off, and might also send resources and information used in UI that isn't necessarily coupled with mongoose. It also allows interactions with the database to be reusable in multiple routes.
I'm trying to wrap my head around how to handle more complicated queries when designing a REST API using Mongo + Express. Unfortunately, all of the examples I've been able to find have been too simple. Here's a simple example for the purpose of this question. I have Groups and I have Users. Within each Group, there are members and 1 leader. For the sake of simplicity, I am going to exclude middleware and post/put/delete functionality.
The routes would look something like this:
app.get('/groups', groups.all);
app.get('/groups/:groupId', groups.show);
app.param('groupId', groups.group);
The controller would look something like this:
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Group = mongoose.model('Group'),
_ = require('lodash');
/**
* Find group by id
*/
exports.group = function(req, res, next, id) {
Group.load(id, function(err, group) {
if (err) return next(err);
if (!group) return next(new Error('Failed to load group ' + id));
req.group = group;
next();
});
};
/**
* Show a group
*/
exports.show = function(req, res) {
res.jsonp(req.group);
};
/**
* List of Groups
*/
exports.all = function(req, res) {
Group.find().sort('-created').populate('user', 'name username').exec(function(err, groups) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(groups);
}
});
};
And then the model would look something like this:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Group Schema
*/
var GroupSchema = new Schema({
created: {
type: Date,
default: Date.now
},
updated: {
type: Date,
default: Date.now
},
enableScores: {
type: Boolean,
default: false
},
name: {
type: String,
default: '',
trim: true
},
creator: {
type: Schema.ObjectId,
ref: 'User'
},
commissioner: {
type: Schema.ObjectId,
ref: 'User'
}
});
/**
* Validations
*/
GroupSchema.path('name').validate(function(name) {
return name.length;
}, 'Name cannot be blank');
/**
* Statics
*/
GroupSchema.statics.load = function(id, cb) {
this.findOne({
_id: id
})
.populate('creator', 'name username')
.populate('commissioner', 'name username')
.exec(cb);
};
mongoose.model('Group', GroupSchema);
What if I want to query the REST API based off the commissioner field? Or the creator, name, or created field? Is this possible using Express's routing and if so, is there a best practice?
It seems like instead of handling each of these unique situations, it would be better to have something generic that returns all groups that match based off of req.params because if the model changes later down the line, I won't need to update the controller. If this is the way to do it, then perhaps modifying the all() function to find based off of the req.params is the solution? So if nothing is provided, then it returns everything, but as you provide more get parameters, it drills down on what you are looking for.
I would recommend using req.query for matching fields in your schema.
If you send a request like /groups?name=someGrp&enableScores=1 the req.query will look like this...
{name: "someGrp", enableScores: 1}
You can pass this object to the find method like this...
Group.find(req.query, callback);
This approach will work for simple property matching queries but for other things like comparisons and array properties you will have to write additional code.