Currently I'm working on a REST API in Node JS & Express. I have to create a link for every resource in the API like the example below.
{
"id": "20281",
"title": "test",
"body": "test",
"date": "2017-11-14 09:01:35",
"_links": {
"self": {
"href": "href"
},
"collection": {
"href": "href"
}
}
},
I know I can handle this in my controller because the req object is available. But it would be better to create a virtual field in my model to create the link on the fly and not save it in the DB.
What would be the best way to do this?
DB: MongoDB
ODM: Mongoose
There are a lot of ways to do this, and you haven't described your DB or ORM or the like, but implied that you have something along those lines with your terminology.
The simplest thing to do would be to just assume that this is not a concern of the storage, but instead it's something that you'd apply globally to the api, so that all routes have it applied. This could be in the form of middleware:
router.use((req, res, next) => {
if (res.data) { // if you do it this way, none of your routes will actually do a res.send, but instead will put their data into res.data and rely on another middleware to render or send it
res.data._links = {
self: calculateSelf(req, res.data),
collection: calculateCollection(req, res.data)
};
}
});
Given that, those two link calculators could use some standard patterns or regex to figure out what the link should look like generically.
Alternatively, if you're using Mongoose, you could override toJSON to populate those links in any models you expect to send down the wire, but that implies that your models should be aware of what the root application URL is, which I don't recommend.
Virtual field implementation:
YourSchema.virtual('links').get(() => {
return {
self: 'http://example.com/api/v1.0/schema/' + this._id,
collection: 'http://example.com/api/v1.0/schema/'
};
});
For that to work, you have to pass { virtuals: true } to toObject() or toJSON() or set it as a global mongoose configuration to always show virtuals. As I said before, though, I really wouldn't recommend that as it requires the schemas to have access to and knowledge of the base URL, which can change between environments. If (as your schema implies) the model is representing a web page, then the URL template could be something that is actually relevant to the model, but in most domains that wouldn't be the case and so I'd recommend the middleware approach.
If you are using mongoose you can use virtual properties to create a non persisting fields, Here is an example:
var MySchema = new Schema({
id: Number,
title: String,
body: String,
date: Date
});
MySchema.virtual('links').get(function () {
return {
self: {
href: 'https://docent.cmi.hro.nl/bootb/demo/notes/' + this.id
},
collection: {
href: 'https://docent.cmi.hro.nl/bootb/demo/notes'
}
}
});
var My = mongoose.model('My', MySchema);
Or you can use after find middleware (post hooks), example:
var MySchema = new Schema({
id: Number,
title: String,
body: String,
date: Date
});
MySchema.post('find', function(doc) {
doc.links = {
self: {
'href': 'https://docent.cmi.hro.nl/bootb/demo/notes/' + doc.id
},
collection: {
'href': 'https://docent.cmi.hro.nl/bootb/demo/notes/'
}
}
});
var My = mongoose.model('My', MySchema);
Related
Hello I am new to nodejs and mongodb.
I have 3 models:
"user" with fields "name phone"
"Shop" with fields "name, address"
"Member" with fields "shop user status". (shop and user hold the "id" of respective collections).
Now when I create "shops" api to fetch all shop, then I need to add extra field "isShopJoined" which is not part of the model. This extra field will true if user who see that shop is joined it otherwise it will be false.
The problem happens when I share my model with frontend developers like Android/iOS and others, They will not aware of that extra field until they see the API response.
So is it ok if I add extra field in shops listing which is not part of the model? Or do I need to add that extra field in model?
Important note
All the code below has NOT been tested (yet, I'll do it when I can setup a minimal environment) and should be adapted to your project. Keep in mind that I'm no expert when it comes to aggregation with MongoDB, let alone with Mongoose, the code is only here to grasp the general idea and algorithm.
If I understood correctly, you don't have to do anything since the info is stored in the Member collection. But it forces the front-end to do an extra-request (or many extra-requests) to have both the list of Shops and to check (one by one) if the current logged user is a Member of the shop.
Keep in mind that the front-end in general is driven by the data (and so, the API/back-end), not the contrary. The front-end will have to adapt to what you give it.
If you're happy with what you have, you can just keep it that way and it will work, but that might not be very effective.
Assuming this:
import mongoose from "mongoose";
const MemberSchema = new mongoose.Schema({
shopId: {
type: ObjectId,
ref: 'ShopSchema',
required: true
},
userId: {
type: ObjectId,
ref: 'UserSchema',
required: true
},
status: {
type: String,
required: true
}
});
const ShopSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
address: {
//your address model
}
});
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
phone: {
type: String,
required: true,
},
// Add something like this
shopsJoined: {
type: Array,
default: [],
required: true
}
});
You could tackle this problem via 2 ways:
MongoDB Aggregates
When retrieving (back-end side) the list of shops, if you know the user that made the request, instead of simply returning the list of Shops, you could return an aggregate of Shops and Members resulting in an hybrid document containing both the info of Shops and Models. That way, the front-end have all the info it needs with one back-end request.
Important note
The following code might not work as-is and you'll have to adapt it, I currently have nothing to test it against. Keep in mind I'm not very familiar with aggregates, let alone with Mongoose, but you'll get the general idea by looking the code and comments.
const aggregateShops = async (req, res, next) => {
try {
// $lookup will merge the "Model" and "Shop" documents into one
// $match will return only the results matching the condition
const aggreg = await Model.aggregate({$lookup: {
from: 'members', //the name of the mongodb collection
localField: '_id', //the "Shop" field to match with foreign collection
foreignField: 'shopId', //the "Member" field to match with local collection
as: 'memberInfo' //the field name in which to store the "Member" fields;
}, {
$match: {memberInfo: {userId: myUserId}}
}});
// the result should be an array of object looking like this:
/*{
_id: SHOP_OBJECT_ID,
name: SHOP_NAME,
address: SHOP_ADDRESS,
memberInfo: {
shopId: SHOP_OBJECT_ID,
userId: USER_OBJECT_ID,
status: STATUS_JOINED_OR_NOT
}
}*/
// send back the aggregated result to front-end
} catch (e) {
return next(e);
}
}
Drop the Members collection and store the info elsewhere
Instinctively, I would've gone this way. The idea is to either store an array field shopsJoined in the User model, or a membersJoined array field in the Shops model. That way, the info is retrieved no matter what, since you still have to retrieve the Shops and you already have your User.
// Your PATCH route should look like this
const patchUser = async (req, res, next) => {
try {
// How you chose to proceed here is up to you
// I tend to facilitate front-end work, so get them to send you (via req.body) the shopId to join OR "un-join"
// They should already know what shops are joined or not as they have the User
// For example, req.body.shopId = "+ID" if it's a join, or req.body.shopId = "-ID" if it's an un-join
if (req.body.shopId.startsWith("+")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $push: { shopsJoined: req.body.shopId } }
);
} else if (req.body.shopId.startsWith("-")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $pull: { shopsJoined: req.body.shopId } }
);
} else {
// not formatted correctly, return error
}
// return OK here depending on the framework you use
} catch (e) {
return next(e);
}
};
Of course, the above code is for the User model, but you can do the same thing for the Shop model.
Useful links:
MongoDB aggregation pipelines
Mongoose aggregates
MongoDB $push operator
MongoDB $pull operator
Yes you have to add the field to the model because adding it to the response will be only be a temporary display of the key but what if you need that in the future or in some list filters, so its good to add it to the model.
If you are thinking that front-end will have to be informed so just go it, and also you can set some default values to the "isShopJoined" key let it be flase for the time.
Fastify has some really awesome json schema support. (Link)
However, I now want to use the schemas which I added with fastify.addSchema(..) inside my business logic as well. For example (pseudo code):
schema = fastify.getSchema("schema1")
if (schema.validate(data)) {
console.log("ok");
} else {
console.log("not ok");
}
How can I achieve that?
Right now, in Fastify, one route has a set of validation functions.
These functions exist only because you set them in the { schema: {} } route
configuration.
So, in the first place, if you don't set those schemas in a route, you will be not able to access them.
The getSchema function retrieves the schema object, not che compiled function.
The relation is not 1:1 because a validation function may use more schemas via the $ref keyword.
The only way to archive what you need is to monkey patch the internal Fastify (highly discouraged)
Or open a feature request to the project.
Here an example, and as you can see, you are limited to get the route's validation functions inside the route's context.
So, it is far from being a flexible usage.
const fastify = require('fastify')({ logger: true })
const {
kSchemaBody: bodySchema
} = require('fastify/lib/symbols')
fastify.post('/', {
schema: {
body: {
$id: '#schema1',
type: 'object',
properties: {
bar: { type: 'number' }
}
}
}
}, async (request, reply) => {
const schemaValidator = request.context[bodySchema]
const result = schemaValidator({ bar: 'not a number' })
if (result) {
return true
}
return schemaValidator.errors
})
fastify.inject({
method: 'POST',
url: '/',
payload: {
bar: 33
}
}, (err, res) => {
console.log(res.json())
})
I'm writing a graphql project with express. I had defined User object like this:
const User = new GraphQLObjectType({
name: 'User',
fields: () => ({
name: {
type: new GraphQLNonNull(GraphQLString),
},
friends: {
type: HistoricalPerformanceEvaluation,
async resolve(user) {
return db.users.findFriendsFor(user.id);
},
},
}),
});
However, i'd like to use graphql syntax to create the schema and define it like this:
type User {
name: String
friends: [User]!
}
Where should i write the resolver now? I'm not using Apollo.
You can use addResolveFunctionsToSchema function from 'graphql-tools' to create schema with resolver functions.
import { addResolveFunctionsToSchema } from 'graphql-tools';
Using the reference graphql-js library, you can pass a root object to the GraphQL execution. Any top-level queries and mutations are looked up on this object, and further nested query fields are looked up on the objects returned from that, and so on.
The example on the graphql-js front page includes this object:
// The root provides a resolver function for each API endpoint
var root = {
hello: () => {
return 'Hello world!';
},
};
That object is then passed as a parameter to the graphql entry point.
I'm new with node.js. I'm writing a single rest API and I want to know what is the right way to expose data without expose unnecessary information.
In my example, I have a "Tag" schema. And I don't want to expose the mongoose fields to my client. My code:
apiRoutes.get('/tag', passport.authenticate('jwt', {
session: false
}), (req, res) => {
Tag.find({}, (err, tags) => {
return res.json(tags);
});
});
But in client, I don't want to expose "_id" and "__v":
{
"_id": "57083a5e725f3cf0242a2916",
"tagName": "Test",
"en_us": "Testing",
"__v": 0,
"lastUpdated": "2016-04-08T23:10:22.759Z"
}
What is the right way to map only relevant fields?
Your can always use mongoose virtuals. Here is an example:
In your model, you can use something like this:
Tag
.virtual('public')
.get(function() {
return {
tagName: this.tagName,
en_us: this.en_us,
lastUpdated: this.lastUpdated
};
});
Then, when making a query, just use the virtual you've created:
Tag.find({}, (err, tags) => {
res.json(tags.map(tag => tag.public));
});
You can use Schema transform toJSON. Here is a blog post explains it in details ignore certain fields from mongoose schema when return object to client
TagSchema.set('toJSON', {
transform: function(doc, ret, options) {
delete ret._id;
delete ret.__v;
return ret;
}
});
You can declare required fields separated by space in find method:
Tag.find({}, 'tagName en-us', (err, tags) => {
return res.json(tags);
});
Check mongoose documenation.
I have a rest api resource that accepts a JSON post. Example:
{
"location": {
"coordinates": [
-122.41941550000001,
37.7749295
]
}
The coordinates are then collected from the request by Express:
module.exports.create = function(req, res, next) {
var coordinates = req.body.location.coordinates;
....
These are then submitted to a Mongoose model. I am writing tests against this where the location.coordinates is missing e.g.
{
"foo": {
"bar": [
-122.41941550000001,
37.7749295
]
}
This then fails within the validation section of the Model with :
locationSchema.path('location.coordinates').validate(function(coordinates){
^
TypeError: Cannot call method 'validate' of undefined
So my question is how would I validate that the input is correct? Should this be done in the route before getting to the model, or should it be done in the model? Any examples of how would also be appreciated.
For reference the Mongoose model looks something like:
var locationSchema = new Schema({
userid: { type: Number, required: true },
location: {
type: [{
type: "String",
required: true,
enum: ['Point', 'LineString', 'Polygon'],
default: 'Point'
}], required: true,
coordinates: { type: [Number], required:true }
},
create_date: { type: Date, default: Date.now }
});
locationSchema.path('location.coordinates').validate(function(coordinates){
...
}, 'Invalid latitude or longitude.');
My typical approach is to introduce a service layer in between the routes and the model, and that's where the validation happens. Don't think "service" in the "web service" sense; it simply provides an abstraction level around a given domain. This has the following benefits:
It gives you a common abstraction for dealing with persisted and/or external data. That is, whether you're interacting with data from Mongoose or an external web service, all of your route logic can simply interact with a consistent interface.
It provides sound encapsulation around persistence details, allowing you to swap out the implementation without effecting all of your routes.
It allows you to re-use code with non-route consumers (such as an integration test suite).
It provides a good layer for mocking (for use with unit tests, for example).
It provides a very clear "validation and business logic happens here" layer, even when your data is spread across several different databases and/or backend systems.
Here's a simplified example of what that might look like:
location-service.js
var locationService = module.exports = {};
locationService.saveCoordinates = function saveCoordinates(coords, cb) {
if (!isValidCoordinates(coords)) {
// your failed validation response can be whatever you want, but I
// like to reserve actual `Error` responses for true runtime errors.
// the result here should be something your client-side logic can
// easily consume and display to the user.
return cb(null, {
success: false,
reason: 'validation',
validationError: { /* something useful to the end user here */ }
});
}
yourLocationModel.save(coords, function(err) {
if (err) return cb(err);
cb(null, { success: true });
});
};
some-route-file.js
app.post('/coordinates', function(req, res, next) {
var coordinates = req.body.location.coordinates;
locationService.saveCoordinates(coordinates, function(err, result) {
if (err) return next(err);
if (!result.success) {
// check result.reason, handle validation logic, etc.
} else {
// woohoo, send a 201 or whatever you need to do
}
});
});
I've applied this structure to 3 or 4 different web apps and APIs at this point, and have grown quite fond of it.
In my opinion the validation should occur at the very beginning, on the client at first, then in the route.
There's not much interest in passing around invalid data, using resources for nothing, so the sooner you flag it as invalid, the sooner you free the resources.
to check existence of your coordinates, you can use :
if(req.body.location.coordinates){
//do your thing
}