Why isn't my ApolloGraphQL Mutation triggering an error message when the specified condition has been satisfied?

This is my initial query on this platform.

I have successfully implemented login/register mutations, but I am now looking to add error handling functionality. Specifically, I want to display an error message stating "username incorrect" when the user enters an invalid username and a similar message for an incorrect password entry.

Currently, the error message only appears when an invalid username is entered, not when the password is incorrect. In the case of an incorrect password entry, it returns a null user object, as shown below:

{
  "data": {
    "login": {
      "user": null
    }
  }
}

The register mutation works as intended, but fails to display errors when certain conditions are met, resulting in a null user response.

Below is the code snippet, with the login mutation located at the bottom and the register mutation above it.

Thank you!

import { User } from '../entities/User';
import { MyContext } from 'src/types';
import argon2 from 'argon2';

import {
  Resolver,
  Mutation,
  Arg,
  InputType,
  Field,
  Ctx,
  ObjectType,
  Query,
} from 'type-graphql';

@InputType()
class UsernamePasswordInput {
  @Field()
  username: string;
  @Field()
  password: string;
}

@ObjectType()
class FieldError {
  @Field()
  field: string;
  message: string;
}

@ObjectType()
class UserResponse {
  @Field(() => [FieldError], { nullable: true })
  errors?: FieldError[];

  @Field(() => User, { nullable: true })
  user?: User;
}

// SAVE USER TO DATABASE
@Resolver()
export class UserResolver {
  // GET ALL USERS

  @Query(() => [User])
  getAllUsers(@Ctx() { em }: MyContext): Promise<User[]> {
    return em.find(User, {});
  }

  @Mutation(() => UserResponse)
  async register(
    @Arg('options') options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<UserResponse> {
    // username
    if (options.username.length <= 2) {
      return {
        errors: [
          {
            field: 'username',
            message: 'length must be greater than 2',
          },
        ],
      };
    }
    // password
    if (options.password.length <= 3) {
      return {
        errors: [
          {
            field: 'password',
            message: 'length must be greater than 3',
          },
        ],
      };
    }
    // hash password
    const hashedPassword = await argon2.hash(options.password);
    const user = em.create(User, {
      username: options.username,
      password: hashedPassword,
    });
    await em.persistAndFlush(user);
    return {
      user,
    };
  }

  @Mutation(() => UserResponse)
  async login(
    @Arg('options') options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<UserResponse> {
    const user = await em.findOneOrFail(User, {
      username: options.username,
    });

    if (!user) {
      return {
        errors: [
          {
            field: 'username',
            message: ' incorrect username',
          },
        ],
      };
    }
    // verify the user password
    const valid = await argon2.verify(user.password, options.password);

    // if password is not valid, return errors
    if (!valid) {
      return {
        errors: [
          {
            field: 'password',
            message: 'incorrect password',
          },
        ],
      };
    }
    // return user
    return {
      user,
    };
  }
}

Answer №1

Here are some important pointers to keep in mind:

  1. Utilize JavaScript's native error-handling capabilities for efficient error handling.

  2. Adhere to the GraphQL Validation and Apollo Server Error handling guidelines strictly.

  3. Take advantage of TypeGraphQL's built-in support for argument and input validation using Custom Scalars and class-validator.

Based on the provided notes and assumptions, you can simplify and refactor your code as shown below:

UsernamePasswordInput:

// ...
import { Length } from 'class-validator'

@InputType()
class UsernamePasswordInput {
  @Field()
  @Length(3, 32) // must be between 3 and 32 characters
  username: string;

  @Field()
  @Length(4, 16) // must be between 4 and 16 characters
  password: string;
}

UserResolver:
Note that when validating username and password, we utilize JavaScript's Operator Precedence (Short-circuiting). Additionally, the errors thrown are intentionally vague to prevent exposure of sensitive details, obscuring whether it's the username or password that is incorrect.

// ...
import { AuthenticationError } from 'apollo-server-errors'

@Resolver(() => User)
class UserResolver {
  // ...

  @Mutation(() => User)
  async register(
    @Arg('data') data: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<User> {
    // verification of username and password length has already been performed

    // Hash the password
    const hashedPassword = await argon2.hash(data.password);
    
    // Create a new user
    const user = em.create(User, {
      username: data.username,
      password: hashedPassword,
    });
    await em.persistAndFlush(user);

    // Return the newly created user
    return user;
  }

  @Mutation(() => User)
  async login(
    @Arg('data') data: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<User> {
    // Retrieve user from database without revealing detailed information 
    const user = await em.findOne(User, {
      username: data.username,
    });

    // Validate username and password
    // Operator Precedence (Short-circuiting)
    // Maintain generic authentication error message
    if (!user ||
        !(await argon2.verify(user.password, data.password))
    ) throw new AuthenticationError("Invalid username or password");

    // ...

    // Return the authenticated user
    return user;
  }
}

Answer №2

To incorporate an error message within a GraphpQl mutation, you can utilize the native Javascript Error object. For instance:

     const itemExists = await collection.checkForItem(itemId);

    if (!itemExists) {
      return new Error("The specified item does not exist");
    }

Upon receiving this error from the backend, you can extract and display the error message on your application's frontend like so:

{
  "errors": [
    {
      "message": "The specified item does not exist",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
    
    ...
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

ApolloClient encounters type mismatches with ApolloLink

Struggling with creating ApolloClient using TypeScript, encountering type-errors that I'm unable to resolve. Seeking guidance on what steps to take next. Provided below is a snippet of the code (functions fine with JavaScript) for setting up the clie ...

Error: Property 'mytest' is undefined and cannot be read

While working with Swagger API, I encountered an error message when calling the endpoint stating "Cannot read property 'mytest' of undefined" class UserData { private mytest(req:any, res:any, next:any){ return res.json('test32423423&a ...

Optional parameter left unassigned is not automatically determined as undefined

Why is TypeScript not inferring the optional parameter in this function as undefined when it's omitted from the call? function fooFunc<T extends number | undefined>(param?: T){ let x: T extends undefined ? null : T x = param ?? null as any ...

Refreshing the side menu in Ionic 3 post-login

When it comes to displaying side menu items based on the user's role, I run into an issue in app.html. Even though I check if the page's role matches the logged-in role, the menu items don't show up right after logging in. However, upon refr ...

The use of 'import ... =' is restricted to TypeScript files

Error: Oops! Looks like there's a hiccup in the code... 'import ... =' is exclusive to TypeScript files. Expecting '=' here. Don't forget the ';'. Unexpected keyword or identifier popping up! package.json ...

Update the form field with today's date in a JHipster application

In our current project in JHipster, we are facing a challenge with setting the default value of a form field as the current date. JHipster offers a moment format for dates, which is essentially an ngbdatepicker. However, when attempting to change the inpu ...

When using videojs, I have the ability to include a Text Track event handler, however, there is currently no option to remove it

I implemented a listener for the 'cuechange' event on a Text Track and it's functioning correctly. However, I am unable to figure out how to remove this listener. I have attempted the instructions below to remove the listener, but it continu ...

Issue encountered: In TypeScript, an error has been identified in the file three-core.d.ts located in the node_modules directory. Specifically, at line 767 and character 24, the error code TS2304

Encountering an issue: TypeScript error: node_modules/@types/three/three-core.d.ts(767,24): Error TS2304: Cannot find name 'Iterable'. Take a look at the screenshot for reference Following the gulp workflow instructions from this guide: ht ...

struggling to set default values for route parameters in Angular

I'm struggling with setting default values for my route parameters within the ts class of my component. I would like genreId to default to null, while monetization, sortBy, and page should have preset values. I'm unsure whether I should handle th ...

Utilizing a custom class to perform an HTTP POST request in Angular/TypeScript by implementing the `toString()` method

When working with Angular Reactive forms, I encountered an issue while sending the form.value data to an asp.net web-api. The code snippet includes a custom class called len, which emulates a simplified version of a TimeSpan. Here is an example of my requ ...

Troubleshooting Chartjs 3.x Migration: Addressing Animation Issues

While I was in the process of updating my Chartjs charts code, I referred to the migration guide provided: https://www.chartjs.org/docs/next/getting-started/v3-migration.html Most of the mentioned changes worked smoothly, but I encountered some issues wi ...

The combination of Object.keys() and the find function

Having trouble figuring out why I'm getting an error when attempting to use ES6 .find on the following data in order to retrieve the record with id number 3. { {id:10,title:'Dairy & Eggs'} {id:7,title:'Laundry & Household'} {id ...

Exploring Heroes in Angular 2: Retrieving Object Information by Clicking on <li> Items

Currently, I am delving into the documentation for an angular 4 project called "Tour of Heroes" which can be found at https://angular.io/docs/ts/latest/tutorial/toh-pt2.html. <li *ngFor="let hero of heroes" (click)="onSelect(hero)">{{hero.name}}< ...

Having trouble importing a variable from a precompiled library in TypeScript JavaScript

Here is the content of my package.json file: { "name": "deep-playground-prototype", "version": "2016.3.10", "description": "", "private": true, "scripts": { "clean": "rimraf dist", "start": "npm run serve-watch", "prep": "browserify ...

Exploring the Synergy of Nestjs Dependency Injection with Domain-Driven Design and Clean Architecture

I am currently exploring Nestjs and experimenting with implementing a clean-architecture structure. I would appreciate validation of my approach as I am unsure if it is the most effective way to do so. Please note that the example provided is somewhat pseu ...

Output specification: Mandate certain attributes of a designated kind, while permitting them to be incomplete

I am searching for a solution in TypeScript that enforces the return type of a method to be a subset of an interface. Essentially, this means that all properties on the returned object must exist on the interface, but they are not required. Background: De ...

Convert the generic primitive type to a string

Hello, I am trying to create a function that can determine the primitive type of an array. However, I am facing an issue and haven't been able to find a solution that fits my problem. Below is the function I have written: export function isGenericType ...

Broaden the attributes of an existing function

I am currently developing a Koa web server and I am exploring if it's feasible to include an additional parameter to an already established method on the Koa.app object. const mongoState = await connectToDatabase(); app.use(async (ctx, next) => ...

Tips for effortlessly incorporating a new chip while maintaining a neat arrangement and ensuring button alignment

I am looking to enhance my website by adding chips with new tags in a neatly organized manner. Specifically, I want each new chip to be positioned next to the previous one and have the add-button shift to the right side, always staying in front of the last ...

Guide on creating several TypeScript interfaces that share identical type structures

export interface UserFailureResponse { statusCode: number statusMessage: string } export interface UserCreateResponse { statusCode: number statusMessage: string } export interface AuthCheckResponse { statusCode: number statusMessa ...