Search code examples
javascriptangulargraphqles6-promiseapollo

my graphql server mutation return null value


I am having challenges retrieving the results of my mutation. I need to create a db record and send an email notifying to user that the registration was successful. since both the sending of the email and the db update is server side I want to do both in the same mutation. If the email message fail the db must not be updated. So I have the following Mutation:

Mutation: {
        createDealer(_, params) {
            console.log("params: " + JSON.stringify(params));

            bcrypt.genSalt(10, function(err, salt) {
                bcrypt.hash(params.dealer.password, salt, function(err, hash) {
                    // Store hash in your password DB.
                    console.log("hashed password " + params.dealer.password)
                    params.dealer.password = hash;
                    console.log("hashed password " + params.dealer.password + " Hash: " + hash);
                    let session = driver.session();


                    let query = "CREATE (d:Dealer {email:$dealer.email}) SET d += $dealer RETURN d";
                    let here = "here".link("[email protected]");

                    let messageObj = {
                        to: params.dealer.email,
                        subject: 'Dealer Registration',
                        text: `Thank you for signing up. To complete and activate your registration please click ${here}.`
                    }

                    return (sendEmail(messageObj))
                        .then(data => {
                            console.log('SendMail data....' + JSON.stringify(data));
                            return session.run(query, params)
                        })
                        .then(result => {
                            console.log('SendNeo4j data....' + JSON.stringify(result));
                            return result.records[0].get("d").properties
                        })
                        .catch((err) => {
                            console.log(err);
                        });
                    //});
                });
            }); // genSalt
        } // Create Dealer
    }, // Mutation

Even thought both actions are successful I can't seem to retrieve the results. I get 'undefined' for: console.log('SendMail data....' + JSON.stringify(data)); while console.log('SendNeo4j data....' + JSON.stringify(result)); does display the correct data

but graphiql returns 'null' for the mutate. this is the graphiql mutation:

mutation CreateDealer($dealer: DealerInput!) {
  createDealer(dealer: $dealer) {
    email
    name
  }
}

with the DealerInput variables of course.

I have read where you can retrieve multiple results from a query/mutation but I am not sure how it works. Here I need both the results of the sendEmail and the db update for my Angular/apollo front-end....I would imaging graphiql knows nothing of the sendEmail but I expected it to return the properties I requested.

SendEmail:

module.exports = (message) =>
    new Promise((resolve, reject) => {
        const data = {
            from: '[email protected]',
            to: message.to,
            subject: message.subject,
            text: message.text
        };

        mailgun.messages().send(data, (error) => {
            if (error) {
                return reject(error);
            }
            return resolve();
        });
    });

Can someone with a little more experience than I help me out here...thanks


Solution

  • Couple of things to fix here. Returning a Promise (or any other value) inside a callback doesn't do anything, and doing so won't let you chain additional Promises like you want. Instead, your promise gets fired off inside the callback and isn't awaited.

    As a general rule of thumb, don't mix Promises and callbacks. If you absolutely have to use callbacks, always wrap the callback in a Promise (like you did inside sendMail). Luckily, most popular libraries today support both callbacks and Promises. Here's how you could refactor the code above to correctly chain all your Promises:

    createDealer(_, params) {
      return bcrypt.hash(params.dealer.password, 10) // note the return here!
        .then(hash => {
          params.dealer.password = hash
          const session = driver.session()
          const query = "CREATE (d:Dealer {email:$dealer.email}) SET d += $dealer RETURN d"
          const here = "here".link("[email protected]")
          const messageObj = {
             to: params.dealer.email,
             subject: 'Dealer Registration',
             text: `Thank you for signing up. To complete and activate your registration please click ${here}.`
          }
          return sendEmail(messageObj) // note the return here!
        }).then(data => {
          return session.run(query, params) // note the return here!
        }).then(result => {
          result.records[0].get("d").properties // note the return here!
        })
    
    • bcrypt.hash will autogenerate the salt for you if you don't pass one in -- there's no need to call two separate functions
    • We kick off our Promise chain with bcrypt.hash, so we need to return the Promise it returns. A resolver must return a value or a Promise that will resolve to a value, otherwise it returns null.
    • Inside each then, we return a Promise. This way we "chain" our Promises, allowing the final value we return in the resolver to be the value the very last Promise in the chain resolves to.

    We need to also fix your sendMail function to actually return the value. You're correctly returning the new Promise inside the function, but you also need to pass the returned data object to resolve. That tells the Promise to resolve to that value.

    module.exports = (message) => new Promise((resolve, reject) => {
      const data = // ...etc
      mailgun.messages().send(data, (error) => {
        if (error) reject(error) // no need to return, it's pointless
        resolve(data) // pass data to resolve
      })
    })
    

    Side note: looks like the official mailgun library supports Promises.

    Additionally, I would strongly encourage you to look into using async/await, especially when dealing with a long Promise chain. It's less error prone and more readable:

    createDealer async (_, params) {
      const hash = await bcrypt.hash(params.dealer.password)
      params.dealer.password = hash
      const session = driver.session()
      const query = "CREATE (d:Dealer {email:$dealer.email}) SET d += $dealer RETURN d"
      const here = "here".link("[email protected]")
      const messageObj = {
        to: params.dealer.email,
        subject: 'Dealer Registration',
        text: `Thank you for signing up. To complete and activate your registration please click ${here}.`
      }
      const emailResult = await sendEmail(messageObj)
      const result = await session.run(query, params)
      return result.records[0].get("d").properties // still need to return!
    }
    

    EDIT: With regard to catching errors, GraphQL will catch any errors thrown by your resolver, which means you can often skip using catch yourself. For example, if your mailgun request fails, it'll generate some kind of error and your query will return null for data and the error details inside of the errors array.

    That may be sufficient, although 1) you may want to log your error's stack elsewhere; and 2) in production, you probably don't want to expose internal error details to the public.

    That means you'll probably want to use custom errors. As a bonus, you can add some custom properties to your errors to help the client deal with them eloquently. So your code may end up looking more like this:

    class DeliveryFailureError extends Error {}
    DeliveryFailureError.code = 'DELIVERY_FAILURE'
    DeliveryFailureError.message = 'Sorry, we could not deliver the email to your account'
    
    try {
      await mailgun.messages.create()
    } catch (err) {
      logger.error('Mailgun request failed:', err.stack)
      throw new DeliveryFailureError()
    }