How to handle error and send response in GraphQL - javascript

I was starting with GraphQL and I was unable to comprehend how we can throw errors in GraphQL
I went through a couple of articles on the web but almost all of them use Apollo and the code-structure looks very different than how I work.
Consider this piece of code, here where I am making a mutation, now how can send a response message with error and change headers status message in case of error?
AddNewPersonalInfo: {
type: userDashboardType,
args: {
parameter: {
type: userCreationlInputType
}
},
resolve: async (parent, args, context) => {
args.parameter.userId = context.req.headers.userId
//Check if user info already exsist
const checkIfUserInformationExsist = await getSelectedThingFromTable('CatsWork_personal', 'userId', `${userId}`)
if (checkIfUserInformationExsist[0]) {
const error = {
code: 403,
message: 'User info Already exsist'
}
throw new Error(error)
} else {
try {
const addLinkedinUser = await insertIntheTable('personal', payload)
return true
} catch (err) {
console.error(err)
throw new Error(err)
}
}
}
}

What I have faced in one of my projects, it is hard to set the status code of the response. So, I made some custom error response to identify correct statusCode using express-graphql
Below is the example (What I have used in one of my projects):
--------app.js file--------
const graphqlHTTP = require('express-graphql')
app.use('/graphql', (req, res) => {
graphqlHTTP({
schema: GraphQLSchema, //A GraphQLSchema instance from GraphQL.js. A schema must be provided.
graphiql: true,
context: { req },
formatError: (err) => {
const error = getErrorCode(err.message)
return ({ message: error.message, statusCode: error.statusCode })
}
})(req, res)
})
--------getErrorCode function implementation--------
const { errorType } = require('../constants')
const getErrorCode = errorName => {
return errorType[errorName]
}
module.exports = getErrorCode
--------Constant.js file--------
exports.errorName = {
USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',
SERVER_ERROR: 'SERVER_ERROR'
}
exports.errorType = {
USER_ALREADY_EXISTS: {
message: 'User is already exists.',
statusCode: 403
},
SERVER_ERROR: {
message: 'Server error.',
statusCode: 500
}
}
Now, we are ready to use our setup.
From your query or mutation, you need to require constant file and return custom error:
const { errorName } = require('../constant')
AddNewPersonalInfo: {
type: userDashboardType,
args: {
parameter: {
type: userCreationlInputType
}
},
resolve: async (parent, args, context) => {
args.parameter.userId = context.req.headers.userId
//Check if user info already exsist
const checkIfUserInformationExsist = await getSelectedThingFromTable('CatsWork_personal', 'userId', `${userId}`)
if (checkIfUserInformationExsist[0]) {
const error = {
code: 403,
message: 'User info Already exsist'
}
throw new Error(errorName.USER_ALREADY_EXISTS) // Here you can use error from constatnt file
} else {
try {
const addLinkedinUser = await insertIntheTable('personal', payload)
return true
} catch (err) {
console.error(err)
throw new Error(errorName.SERVER_ERROR) // Here you can use error from constatnt file
}
}
}
}
--------Error response--------
{
error: [{
"statusCode": 403,
"message": "User is already exists."
}],
data: null
}
We just need to write custom error handling from FS side too.
Note:- formatError: is deprecated and replaced by customFormatErrorFn. It will be removed in version 1.0.0. You can refer customFormatErrorFn.

graphql should be an application level layer that shouldn't (see last paragraph why shouldn't and not doesn't) require http to work. Although in 99% of cases it runs on top of http, because of how convenient it is to do so, graphql is itself a layer 7 protocol.
What does that mean in your case? Well, it means you should not mix concepts from HTTP/REST with concepts from graphql and focus on the latter. The headers error code is a HTTP/REST concept, graphql sends errors in the errors field of the response and the nodejs implementation already catches all your errors and adds them to the list. The HTTP status will be always 200, and your clients shouldn't care and consume your graphql api and not a mix of REST with graphql.
That being said, there are couple of things that REST over HTTP does better. So people, including the developers of Apollo, kinda mixed concepts too, mainly because the graphql standard is not complete (aka, it doesn't have a standard/rule for solving all the problems you might encounter while building an API), so people improvised. I wouldn't recommend graphql yet for any serious project.
Reference

You can specify an error function inside graphqlHTTP like this:
app.use("/graphql", graphqlHTTP({
schema,
graphiql: true,
customFormatErrorFn: err => {
try {
err.details = JSON.parse(err.message);
err.message = Array.isArray(err.details.error) ? err.details.error.join(",") : err.details.error;
return err;
} catch {
return err;
}
}
}));
where err.message might contain a JSON object or a string.
you can use those function to generate specific client and server error functions:
const clientError = error => new Error(JSON.stringify({
success: false,
code: 400,
error
}));
const serverError = ({ name, message, stack }) => new Error(JSON.stringify({
success: false,
error: "Server Error",
code: 500,
name,
message,
stack
}));
const userValidationError = err => {
if (err.name === "ValidationError") return clientError(Object.values(err.errors).map(({ message }) => message));
return serverError(err);
}
module.exports = {
clientError,
serverError,
userValidationError
};
userValidationError function is useful if you have a mongodb validation error.
so that you would use it inside resolve function like this:
try {
const createdDocument = await MongooseDoc.create(data);
return createdDocument;
} catch (err) {
throw userValidationError(err);
}
the response would be
{
"errors": [
{
"message": "error details 1,error details 2",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"document"
],
"details": {
"success": false,
"code": 400,
"error": [
"error details 1",
"error details 2"
]
}
}
],
"data": {
"document": null
}
}
if you want to throw a clientError you throw it outside try catch.
Hopefully this code helps someone send dynamic error messages in graphql.

Related

Sequelize update via PATCH, how to process each possible result

I'm creating a rest api for CRUD operations using Sequelize and MySql. I'm using a controller to run an update on a PATCH request to update fields of a product. It technically works, but I feel like there is a more elegant way to handle this.
Sequelize's update method will return an array of objects depending on the results. Array[0] is the number of rows affected by the update (should just be one in my case, as I'm updating by id). Array[1] will return an object with details about the update as well as all the old values and new values. Here's how I'm handling that currently:
//products.controller.js
//Update a single product using id (PUT/PATCH)
const patch = (req, res) => {
const id = req.params.id;
Product.update(req.body, { where: { id }, individualHooks: true })
.then((rowsAffected) => {
//Item not found
if (Object.entries(rowsAffected[1]).length === 0) {
res.status(404).send({
success: false,
status: 404, //Not found
message: `Product with id ${id} not found. Update failed.`,
});
return;
}
//if rowsAffected[0] === 1 then success
if (rowsAffected[0] === 1) { //row changed
res.status(200).send({
success: true,
status: 200,
message: `Product updated.`,
id: id,
payload: req.body,
});
} else {
// if rowsAffected[0] !== 1 then it failed.
res.status(200).send({
success: false,
status: 200, //Not Modified
message: `No fields have changed. Product not updated.`,
});
}
})
.catch((err) => {
res.status(500).send({
success: false,
status: 500,
message:
err.message || "Something went wrong while updating the product.",
});
});
}
As you can see, first I'm checking to see if the the update function returns the product details (meaning it successfully found it in the database). If not then sending 404. Then I check the affected rows. If 1 then success, if 0 then nothing changed. Finally I'm catching any server errors.
I feel like there is a better way rather than having to break down the update function's return (like Object.entries(rowsAffected[1]).length === 0)
This is ok if this is the only way you can check the effects of the update. What I can suggest is putting an abstraction above it.
First thing that checking (rowsAffected[0] === 1) does not make much sense, since the update is idempotent and you end up with the same resource state no matter what the actual values are. If you insist, then I would not pair success: false with a 200 ok status, because failure is failure and it requires an error message and 4xx or 5xx status. So either delete it or convert it into a proper error. Hard to find such a status code, but maybe using 409 conflict is ok in these cases https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 though I would just remove this part of the code. I keep it for the sake of the example.
As of the success and status properties in the body, they don't make much sense either, because they travel in the header, and it is evident from the HTTP standard that 2xx means success, 4xx and 5xx means error. So I would remove those too.
If you don't want to support detailed error codes and exception types and parameters, then just send the error messages and the body can be even a string instead of an object.
Sending the err.message to the consumers is a bad idea by unexpected errors. You don't know what you send out. You need to log them and send something general instead. Communicating errors is always a higher abstraction level stuff, many times. As of the Product with id ${id} not found. Update failed. here adding the id is not necessary, because the request contains it.
So atm. the code looks like this:
const patch = (req, res) => {
const id = req.params.id;
Product.update(req.body, { where: { id }, individualHooks: true })
.then((rowsAffected) => {
if (Object.entries(rowsAffected[1]).length === 0) {
res.status(404).send({message: `Product not found. Update failed.`});
return;
}
//if rowsAffected[0] === 1 then success
if (rowsAffected[0] === 1) { //row changed
res.status(200).send({
message: `Product updated.`,
id: id,
payload: req.body,
});
} else {
res.status(409).send({message: "No fields have changed. Product not updated."});
}
})
.catch((err) => {
res.status(500).send({message: "Something went wrong while updating the product."});
});
}
We can go further by mapping status codes to status messages and extracting the possibly repeating parts of the story into separate functions.
const patch = (req, res) => {
const id = req.params.id;
const statusMessages = {
200: "Product updated."
404: "Product not found. Update failed."
409: "No fields have changed. Product not updated.",
500: "Something went wrong while updating the product."
};
Product.update(req.body, { where: { id }, individualHooks: true })
.then(updateStatusVerification)
.then(successHandler(res, statusMessages, () => {
return {
id: id,
payload: req.body,
};
}))
.catch(apiErrorHandler(res, statusMessages));
}
function successHandler(res, statusMessages, callback){
return function (){
let body = callback();
body.message = statusMessages[200];
res.status(200).send(body);
};
}
function apiErrorHandler(res, statusMessages){
return function (err){
let statusCode = 500;
if (err instanceof NotFoundError)
statusCode = 404;
else if (err instanceof NotUpdatedError)
statusCode = 409;
res.status(statusCode).send({
message: statusMessages[statusCode]
});
};
}
function updateStatusVerification(rowsAffected){
return new Promise((resolve, reject) => {
if (Object.entries(rowsAffected[1]).length === 0)
reject(new NotFoundError);
else if (rowsAffected[0] !== 1)
reject(new NotUpdatedError);
else
resolve();
});
}
class ApiError extends Error {}
class NotFoundError extends ApiError {}
class NotUpdatedError extends ApiError {}
We can move the status messages to the documentation. So you will end up with something like this and some utility functions:
const patch = (req, res) => {
const id = req.params.id;
statusMessages = docs.product.update.statusMessages;
Product.update(req.body, { where: { id }, individualHooks: true })
.then(updateStatusVerification)
.then(successHandler(res, statusMessages, () => {
return {
id: id,
payload: req.body,
};
}))
.catch(apiErrorHandler(res, statusMessages));
}
We can go even further if this is a frequent pattern:
const patch = (req, res) => {
const id = req.params.id;
handleUpdate(
Product.update(req.body, { where: { id }, individualHooks: true }),
() => {id: id, payload: req.body},
docs.product.update.statusMessages
);
}
function handleUpdate(dbUpdatePromise, successCallback, statusMessages){
dbUpdatePromise.then(updateStatusVerification)
.then(successHandler(res, statusMessages, successCallback))
.catch(apiErrorHandler(res, statusMessages));
}
So it can be as abstract as you like, it really depends on your needs and what the current usage allows. You can decide how many and what kind of layers you need based on actual use cases and repetitions.

How do I send more than one error at a time when validating?

When I create a POST request for I need to validate the following fields: first_name, last_name, mobile_number, reservation_date, reservation_time and people(party size).
Right now I have a middleware function that checks if any of the fields are missing:
function hasProperties(...properties) {
return function (res, req, next) {
const { data = {} } = res.body;
try {
properties.forEach((property) => {
if (!data[property]) {
const error = new Error(`${property}`);
error.status = 400;
throw error;
}
});
next();
} catch (error) {
next(error);
}
};
}
Then in my controller:
const hasAllProps = hasProperties(
'first_name',
'last_name',
'mobile_number',
'reservation_date',
'reservation_time',
'people'
);
This is working great however I have to add additional validation to several of the fields. I have 2 additional functions: one is making sure the people field is a number, and the other is making sure the reservation_date is a date:
const validPeople = (req, res, next) => {
const { people } = req.body;
if (Number.isInteger(people)) {
return next();
}
next({ status: 400, message: 'people' });
};
const validDate = (req, res, next) => {
const { reservation_date } = req.body;
if (reservation_date instanceof Date) {
return next();
}
next({ status: 400, message: 'reservation_date' });
};
Then I pass them all in to my exports:
create: [hasAllProps, validDate, validPeople]
I am only ever able to send one error at a time, in this case its validDate because it comes before validPeople in the exports array. I am unable to throw all of my errors into an array because I need to response with:
status: 400, message: '<the-specific-field>'
Is there a way to individually send all these error messages?
As the other response has stated, if you're trying to send multiple responses, that's not possible. You can, however, construct an array of the errors.
You could technically pass data between middleware... (Can I send data via express next() function?)
... but my recommendation would be to be to try to merge them into a single middleware. For example, hasAllProps, validPeople, and validDate should ideally all take in a req and return null or an error. Then you could do:
function validDate(req) {
return null;
}
function validOtherProp(req) {
return 'error_here';
}
function anotherValidation(req) {
return 'second_error';
}
const errorCollectorMiddleware = (...validators) =>
(req, res, next) => {
const errors = validators.map(v => v(req)).filter(error => error !== null);
if (errors.length > 0) {
next({
status: 400,
errors
})
} else {
next();
}
}
// This is how you construct a middleware
const middleware = errorCollectorMiddleware(validDate, validOtherProp, anotherValidation);
// And here's a test. You wouldn't do this in your actual code.
console.log(middleware(null, null, console.log))
/*
{
"status": 400,
"errors": [
"error_here",
"second_error"
]
}
*/
With HTTP/S you cannot have one request two responses. The client system sends the request, receives the response and does not expect a second response.

Separating Mongoose code from Express Router

So basically, I'm trying to separate my code that handles data (mongoose) from my express Router code, since I might want to use it elsewhere too.
The first thing I did was, I got rid of the res.json() calls, since I don't want the code to only work returning a http response. I want it to return data, so I can then return that data from my router as a http response, but still use it as regular data elsewhere.
Here is a function I wrote to get data from mongoose.
module.exports.user_login = data => {
console.log(data);
ModelUser.findOne({email: data.email}).then(user => {
if(!user){
console.log({email: 'E-mail address not found'});
return {
status: response_code.HTTP_404,
response: {email: 'E-mail address not found'}
}
}
bcrypt.compare(data.password, user.password).then(isMatch => {
if(!isMatch){
console.log({password: 'Invalid password'});
return {
status: response_code.HTTP_400,
response: {password: 'Invalid password'}
}
}
const payload = {
id: user.id,
email: user.email
};
jwt.sign(
payload,
config.PASSPORT_SECRET,
{
expiresIn: "1h"
},
(err, token) => {
console.log({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
return {
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
}
}
);
});
});
};
When this code gets executed in my route like so:
router.post("/login", (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
console.log("ret", dm_user.user_login(req.body));
});
The log says the return value of user_login() is undefined, even though right before the return statement in user_login() I am logging the exact same values and they are getting logged.
Before I changed it to a log, I tried to store the return value in a variable, but obviously that remained undefined as well, and I got the error: 'Cannot read propery 'status' of undefined' when trying to use the value.
I am definitely missing something..
Well you have an small callback hell here. It might be a good idea to go with async / await and splitting up your code into smaller chunks instead of putting everyhing in 1 file.
I rewrote your user_login function:
const { generateToken } = require("./token.js");
module.exports.user_login = async data => {
let user = await ModelUser.findOne({ email: data.email });
if (!user) {
console.log({ email: "E-mail address not found" });
return {
status: response_code.HTTP_404,
response: { email: "E-mail address not found" }
};
}
let isMatch = await bcrypt.compare(data.password, user.password);
if (!isMatch) {
console.log({ password: "Invalid password" });
return {
status: response_code.HTTP_400,
response: { password: "Invalid password" }
};
}
const payload = {
id: user.id,
email: user.email
};
let response = await generateToken(
payload,
config.PASSPORT_SECRET,
response_code
);
return response;
};
I have moved your token signing method into another file and promisfied it:
module.exports.generateToken = (payload, secret, response_code) => {
return new Promise((res, rej) => {
jwt.sign(
payload,
secret,
{
expiresIn: "1h"
},
(err, token) => {
if (err) {
rej(err);
}
res({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
}
);
});
};
Now you need to change your router function into an async:
router.post("/login", async (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
let result = await dm_user.user_login(req.body);
console.log(result);
});
In addition: You get undefined because you return your value to an callback function
I also would seperate your routes from your controllers instead of writing your code inside an anonymous function
Please notice that whenever you are trying to return any value you are always present in the callback function and that is definitely not going to return any value to its intended place.
There are a couple of things you can improve about your code :
1.Donot use jwt inside your code where you are making database calls, instead move it where your routes are defined or make a separate file.
2.If you are intending to re-use the code, I would suggest you either use async-await as shown in the answer above by Ifaruki or you can use something like async.js. But the above shown approach is better.
Also always use 'error' field when you are making db calls like this:
ModelUser.findOne({email: data.email}).then((error,user) => {

I am trying to implement a dialogflow webook to connect to wordpress api, but getting a Error: No responses defined for platform: null

Here is my code. How can I configure it to return a response for the Google Assistant integration? I would like to use this code within the Dialogflow in-line code editor or deploy it to firebase functions.
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
function getProjects(agent){
const api = 'https://playhacker.com/wp-json/wp/v2';
const tag = agent.parameters.tag;
//let url = `${api}/posts?tags=${tag}&_embed`;
let getPosts = (tag, callback) => {
let url = `${api}/posts?tags=${tag}&_embed`;
request({url}, (err, res, body) => {
if (err) {
callback('Sorry, there was an error getting posts from our blog', err);
return;
} else {
let posts = JSON.parse(body);
if (posts.length === 0) {
callback(`It does not seem like there is any content available on this topic`);
return;
} else {
let formattedPosts = posts.map((post) => {
return {
"payload": {
"google": {
"expectUserResponse": true,
"richResponse": {
"items": [
{
"simpleResponse": {
"textToSpeech": "Here is a project we found for you"
}
},
{
"basicCard": {
"title": post.title.rendered,
"subtitle": "Project",
"formattedText": post.excerpt.rendered.replace(/<(?:.|\\n)*?>/gm, '').replace(/&[^\\s]*/, ''),
"image": {
"url": post._embedded['wp:featuredmedia'][0].media_details.sizes.listing.source_url,
"accessibilityText": "featured image"
},
"buttons": [
{
"title": "Read more",
"openUrlAction": {
"url": post.link
}
}
],
"imageDisplayOptions": "CROPPED"
}
}
]
}
}
}
};
});
formattedPosts.unshift({
type: 0,
platform: 'google',
speech: 'Sure, here are some helpful projects'
});
callback(undefined, formattedPosts);
return;
}
}
});
};
}
let intentMap = new Map();
intentMap.set('getProjects',getProjects);
agent.handleRequest(intentMap);
});
When I run it in Dialogflow in-line editor, I got this error:
{
insertId: "000001-ec48afd6-c286-47e4-8c75-43a2c6f1fc08"
labels: {…}
logName: "projects/apologetic-robot-fiajxe/logs/cloudfunctions.googleapis.com%2Fcloud-functions"
receiveTimestamp: "2020-05-03T06:38:00.862369842Z"
resource: {
labels: {…}
type: "cloud_function"
}
severity: "ERROR"
textPayload: "Error: No responses defined for platform: null
at V2Agent.sendResponses_ (/srv/node_modules/dialogflow-fulfillment/src/v2-agent.js:243:13)
at WebhookClient.send_ (/srv/node_modules/dialogflow-fulfillment/src/dialogflow-fulfillment.js:505:17)
at promise.then (/srv/node_modules/dialogflow-fulfillment/src/dialogflow-fulfillment.js:316:38)
at <anonymous>
at process._tickDomainCallback (internal/process/next_tick.js:229:7)"
timestamp: "2020-05-03T06:37:59.833Z"
trace: "projects/apologetic-robot-fiajxe/traces/29d9241c46088463269b00ecf3558974"
}
The error Error: No responses defined means you haven't defined any response to be sent to dialogflow that can be shown back to the chatbot user.
For eg. when the codeflow reaches the function getProjects, there should be a response back to dialogflow before returning from the function, anything like
agent.add(`There seems to be some error`);
which will in turn show this string back to the user.
Additonally, in your above example, I cannot see your getPosts function being called anywhere.

Handling exceptions in Sails.js

I'm using Sails.js to develop a REST API server.
For ease of use and for abstraction sake I would like to throw exceptions inside of my controllers, e.g.:
// api/controllers/TempController.js
module.exports = {
index: function(request, response) {
throw new NotFoundException('Specific user is not found.');
throw new AccessDeniedException('You have no permissions to access this resource.');
throw new SomeOtherException('Something went wrong.');
}
};
How do I catch those exceptions automatically (on a global level) and transform them into a valid JSON response? e.g.:
{
"success": false,
"exception": {
"type": "NotFoundException",
"message": "Specific user is not found."
}
}
Is it the best possible approach to use built-in serverError response in order to handle such exceptions? Or is it better to create some custom middleware? If so, could you provide a simple example?
The unhandled exceptions are passed to the default response in the api/responses/serverError.js as a first argument data.
Here's an example of how such exceptions can be handled:
var Exception = require('../exceptions/Exception.js');
module.exports = function serverError (data, options) {
var request = this.req;
var response = this.res;
var sails = request._sails;
// Logging error to the console.
if (data !== undefined) {
sails.log.error('Sending 500 ("Server Error") response: \n', String(data));
} else {
sails.log.error('Sending empty 500 ("Server Error") response');
}
response.status(500);
if (data instanceof Exception) {
return response.json({
success: false,
exception: {
type: data.constructor.name,
message: data.message
}
});
} else {
return response.json(data);
}
};
When exception is thrown in the controller:
// api/controllers/TempController.js
var NotFoundException = require('../exceptions/NotFoundException.js');
module.exports = {
index: function(request, response) {
throw new NotFoundException('Specific user is not found.');
}
};
This will output the following JSON:
{
"success": false,
"exception": {
"type": "NotFoundException",
"message": "Specific user is not found."
}
}

Categories