LOOPBACK 4: Add parameters in a API call - node.js

I am new in Loopback 4 (NodeJS) and I have a question. I am developing an API. How can indicate parameters in the body of a post request that are not define as a model?.
Example:
#post('/gameshits/{id}', {
responses: {
'200': {
description: 'Return the number of correct answers',
},
},
})
async gamesHits(
#param.path.string('id') id: string,
#requestBody() answers: Array<String>,
): Promise<number> {
....
}
The problem is in the requestBody()
Its compile but in the loopback/explorer said that it can be render. The only option is create a model? how can add more parameters to send in the body of the call? (not in the url like #param do)
Thanks.

No you don't need to create a model to indicate parameters, actually it's very easy but there's no much documentation about it.
You can indicate something like this.
#post('/gameshits/{id}', {
responses: {
'200': {
description: 'Return the number of correct answers',
},
},
})
async gamesHits(
#param.path.string('id') id: string,
#requestBody({
content: {
'application/json': {
type: 'object',
schema: {
properties: {
gameName: {type: 'string'},
characters: {
type: 'array',
items: {
properties: {
name: {type: 'number'},
power: {type: 'number'},
cost: {type: 'number'},
ability: {type: 'string'}
},
},
},
},
},
},
},
}) data: any,
): Promise<number> {
....
}
To get a loopback/explorer response like this.
{
"gameName": "string",
"characters": [
{
"name": 0,
"power": 0,
"cost": 0,
"ability": "string"
}
]
}

You don't need any documentation but swagger's documentation regarding describing requests: https://swagger.io/docs/specification/2-0/describing-request-body/?sbsearch=request
Then just apply the OpenAPI Specification inside the #requestBody decorator as mentioned by darkunbeknownst.

Related

How to make Swagger UI accepts request body with optional field

Well, the title is very easy to understande, but to make things detailed:
I have an expressjs application, written with TypeScript. The app receives a request, where the JSON body is like this:
{
name: "name",
description: "description",
github: "github",
logo: "logo",
app: "app"
}
But the thing is: The app property is optional, since I will insert the object in the database WITH that property only if the request was made with it declared.
I'd like to know if it's possible to make the Swagger accept that optional thing, and if so, how to do that?
EDIT:
As requested by #Anatoly, this is the swagger route definition:
post: {
summary: 'Create a new project',
tags: ['projects'],
requestBody: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Project'
}
}
}
}
},
components: {
schemas: {
Project: {
type: 'object',
properties: {
name: {
type: 'string'
},
description: {
type: 'string'
},
github: {
type: 'string'
},
logo: {
type: 'string'
},
app: {
type: 'string'
}
}
}
}
}
All params except path ones are optional unless they have the required attribute as true
app:
type:string
required:true
Refer the heading Required and Optional Parameters at https://swagger.io/docs/specification/describing-parameters/

Fastify schema fails to validate type of a property of a nested object

I am new in using Fastify Js. I have this route that fails to validate the type of properties inside the nested object from inside the body schema.
fastify.put('/comment', {
schema: {
body: {
type: 'object',
required: ['from', 'body', 'courseCode'],
properties: {
from: {
type: 'object',
required: ['email', 'username'],
properties: {
// this is the part where validation fails
email: { type: 'string' },
username: { type: 'string' },
}
},
body: { type: 'string' },
courseCode: { type: 'string' },
}
}
}
}, async (req, rep) => {
// logic
return {someResponse};
});
I am using REST Client extension in VSCode. This is my sample request
put http://localhost:3000/comment
Content-Type: application/json
{
"from": {
/* Notice the `email` and `username`, Fastify does not throw an error or some exception
that tells this is suppose to be a string and not an integer */
"email": 3123,
"username": 123123
},
"body": "some comment from enginex",
"courseCode": "d-c"
}
I also try using insomia.io and postman but the result is the same.
Did I miss something important here? Thank You so much.

How to query for sub-document in an array with Mongoose

I have a Schema of Project that looks like this:
const ProjectSchema = new mongoose.Schema({
name: {
type: String,
Required: true,
trim: true
},
description: {
type: String,
},
devices: [{
name: {type: String, Required: true},
number: {type: String, trim: true},
deck: {type: String},
room: {type: String},
frame: {type: String}
}],
cables: {
type: Array
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
adminsID: {
type: Array
},
createdAt: {
type: Date,
default: Date.now
}
I want to query an object from array of "devices".
I was able to add, delete and display all sub-documents from this array but I found it really difficult to get single object that matches _id criteria in the array.
The closest I got is this (I'm requesting: '/:id/:deviceID/edit' where ":id" is Project ObjectId.
let device = await Project.find("devices._id": req.params.deviceID).lean()
console.log(device)
which provides me with below info:
[
{
_id: 6009cfb3728ec23034187d3b,
cables: [],
adminsID: [],
name: 'Test project',
description: 'Test project description',
user: 5fff69af08fc5e47a0ce7944,
devices: [ [Object], [Object] ],
createdAt: 2021-01-21T19:02:11.352Z,
__v: 0
}
]
I know this might be really trivial problem, but I have tested for different solutions and nothing seemed to work with me. Thanks for understanding
This is how you can filter only single object from the devices array:
Project.find({"devices._id":req.params.deviceID },{ name:1, devices: { $elemMatch:{ _id:req.params.deviceID } }})
You can use $elemMatch into projection or query stage into find, whatever you want it works:
db.collection.find({
"id": 1,
"devices": { "$elemMatch": { "id": 1 } }
},{
"devices.$": 1
})
or
db.collection.find({
"id": 1
},
{
"devices": { "$elemMatch": { "id": 1 } }
})
Examples here and here
Using mongoose is the same query.
yourModel.findOne({
"id": req.params.id
},
{
"devices": { "$elemMatch": { "id": req.params.deviceID } }
}).then(result => {
console.log("result = ",result.name)
}).catch(e => {
// error
})
You'll need to use aggregate if you wish to get the device alone. This will return an array
Project.aggregate([
{ "$unwind": "$devices" },
{ "$match": { "devices._id": req.params.deviceID } },
{
"$project": {
name: "$devices.name",
// Other fields
}
}
])
You either await this or use .then() at the end.
Or you could use findOne() which will give you the Project + devices with only a single element
Or find, which will give you an array of object with the _id of the project and a single element in devices
Project.findOne({"devices._id": req.params.deviceID}, 'devices.$'})
.then(project => {
console.log(project.devices[0])
})
For now I worked it around with:
let project = await Project.findById(req.params.id).lean()
let device = project.devices.find( _id => req.params.deviceID)
It provides me with what I wanted but I as you can see I request whole project. Hopefuly it won't give me any long lasting troubles in the future.

removing a string within nested array mongoose

const AllPostsSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
posts: [{
postid: {
type: String
},
title: {
type: String
},
category: {
type: String
},
subcategory: {
type: String
}, category: {
type: String
},
description: {
type: String
},
name: {
type: String
},
price: {
type: Number
},
email: {
type: String
},
phonenumber: {
type: Number
},
language: {
type: String
},
make: {
type: String
},
model: {
type: Number
},
odometer: {
type: Number
},
condition: {
type: String
},
state: {
type: String
},
town: {
type: String
},
city: {
type: String
},
links: [{ type: String }],
date: {
type: Date,
default: Date.now
}
}]
})
AllPosts.findOneAndUpdate({ 'user': req.query.userid },
{ $pull: { 'posts': { 'links': req.query.link } } }
)
.then(post => console.log(post))
i need to find a specific user and within that user match the post id then remove one of the links in links array. when I do it like above it removes the whole array instead i want it to remove specific link within links array in posts arrayy.
Each user has one or more than one posts. I need to update a single post of a specific user. if a user wants to delete an image i delete that from amazon s3 then, i need to remove the link from that post link array so it doesnt create broken img tags in the front end.
AllPosts.findOneAndUpdate({ 'user': req.query.userid, 'posts.postid': req.query.postid },
{ $pull: { 'links': req.query.link } }
)
.then(post => console.log(post))
this also didnt work.
Solved. For future reference :
AllPosts.findOneAndUpdate({ 'user': req.query.userid, 'posts.postid': req.query.postid },
{ $pull: { 'posts.$.links': req.query.link } }
)
.then(post => console.log(post))

Denormalized schema?

Mongoose schema:
const postSchema = new mongoose.Schema({
title: { type: String },
content: { type: String },
comments: {
count: { type: Number },
data: [{
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: { type: String },
created: { type: Date },
replies: [{
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: { type: String },
created: { type: Date },
}]
}]
}
})
I don't want to normalize comments and replies because:
I will always need post comments together (read in a single go)
I will never use comments to query or sort anything
It's so much easier to create, update and delete post with comments (document locking)
If I normalized it, I would have to:
Get post from database
Get comments from database
Get replies for each comment
This is one of the biggest strengths of MongoDB: to group your data specifically to suit your application data needs but GraphQL and Relay.js doesn't seem to support that?
Question:
Is there a way to get comments nested inside of a single post object (or at least get replies nested inside of a single comment) in order to get the whole thing in a single read?
GraphQL schema:
const postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
title: { type: GraphQLString },
content: { type: GraphQLString },
comments: {
// ?????
}
}),
interfaces: [nodeInterface],
})
Update:
const postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
title: { type: GraphQLString },
content: { type: GraphQLString },
commentCount: { type: GraphQLInt },
comments: { type: new GraphQLList(commentType) }
}),
interfaces: [nodeInterface]
})
const commentType = new GraphQLObjectType({
name: 'Comment',
fields: () => ({
content: { type: GraphQLString },
created: { type: GraphQLString },
author: {
type: userType,
resolve: async (comment) => {
return await getUserById(comment.author)
}
},
replies: {
type: new GraphQLList(commentType),
resolve: (comment) => {
// Log is below
console.log(comment)
// Error only occurs if I return it!
return comment.replies
}
}
})
})
Log (comment):
{
author: 'user-1',
created: '05:35'
content: 'Wow, this is an awesome comment!',
replies:
[{
author: 'user-2',
created: '11:01',
content: 'Not really..',
},
{
author: 'user-1',
created: '11:03',
content: 'Why are you so salty?',
}]
}
This is one of the biggest strengths of MongoDB: to group your data specifically to suit your application data needs but GraphQL and Relay.js doesn't seem to support that?
GraphQL does support what you're trying to do. Before going into the details, we need to keep in mind that GraphQL is about exposing data, NOT about how we store data. We can store in whatever way we like - normalized or not. We just need to tell GraphQL how to get the data that we define in the GraphQL schema.
Is there a way to get comments nested inside of a single post object (or at least get replies nested inside of a single comment) in order to get the whole thing in a single read?
Yes, it's possible. You just define the comments as a list. However, GraphQL does not support arbitrary type of field in a GraphQL object type. Therefore, we need to define separate GraphQL types. In your mongoose schema, comments is an object with count of comments and the comments data. The data property is a list of another type of objects. So, we need to define two GraphQL objects - Comment and CommentList. The code looks like below:
const commentType = new GraphQLObjectType({
name: 'Comment',
fields: () => ({
author: { type: GraphQLString },
content: { type: GraphQLString },
created: { type: GraphQLString },
replies: { type: new GraphQLList(commentType) },
}),
});
const commentListType = new GraphQLObjectType({
name: 'CommentList',
fields: () => ({
count: {
type: GraphQLInt,
resolve: (comments) => comments.length,
},
data: {
type: new GraphQLList(commentType),
resolve: (comments) => comments,
},
}),
});
const postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
title: { type: GraphQLString },
content: { type: GraphQLString },
comments: {
type: commentListType,
resolve: (post) => post.comments,
}
}),
interfaces: [nodeInterface],
});
I don't want to normalize comments and replies because:
I will always need post comments together (read in a single go)
I will never use comments to query or sort anything
It's so much easier to create, update and delete post with comments (document locking)
Defining post and comment GraphQL object types in the above way let you:
read all post comments together
not to use comment to query, as it does not even have id field.
implement comments manipulation in whatever way you like with your preferred database. GraphQL has nothing to do with it.

Resources