Unique Email Addresses

In the Registering a User challenge, you updated the register() method in the AuthService to create a new User node in the database.

There is still one // TODO comment remaining in this file.

Currently, it is still possible to use the same email address twice, we should guard against that.

This functionality could be handled by checking the database before running the CREATE Cypher statement, but this could still cause problems if the database is manually updated elsewhere.

Instead, you can pass the responsibility of handling the duplicate user error to the database by creating a Unique Constraint on the :User label, asserting that the email property must be unique.

To pass this challenge, you will need to:

Handling Constraint Errors

If we take a look at register() method, it has been hardcoded to throw a new ValidationError if the email address is anything other than graphacademy@neo4j.com.

js
src/services/auth.service.js
async register(email, plainPassword, name) {
  const encrypted = await hash(plainPassword, parseInt(SALT_ROUNDS))

  // TODO: Handle Unique constraints in the database
  if (email !== 'graphacademy@neo4j.com') {
    throw new ValidationError(`An account already exists with the email address ${email}`, {
      email: 'Email address taken'
    })
  }

  // Open a new session
  const session = this.driver.session()

  // Create the User node in a write transaction
  const res = await session.executeWrite(
    tx => tx.run(
      `
        CREATE (u:User {
          userId: randomUuid(),
          email: $email,
          password: $encrypted,
          name: $name
        })
        RETURN u
      `,
      { email, encrypted, name }
    )
  )

  // Extract the user from the result
  const [ first ] = res.records
  const node = first.get('u')

  const { password, ...safeProperties } = node.properties

  // Close the session
  await session.close()

  return {
    ...safeProperties,
    token: jwt.sign(this.userToClaims(safeProperties), JWT_SECRET),
  }
}

The code also has no explicit error handling. Any errors will be sent back up the stack and will result in a 500 Bad Request error. Instead, this error should be caught and reformatted in such a way that the server would return a 422 Unprocessable Entity error. This way, the error can be better handled by the UI.

To do this, you will need to rearange the code into try/catch blocks.

If we take a look at a generic try/catch, it can be broken down into three parts; try, catch and finally.

js
Try/Catch/Finally
try {
  // Attempt the code inside the block
}
catch (e) {
  // If anything goes wrong in the try block,
  // deal with the error here
}
finally {
  // Run this statement regardless of whether an error
  // is thrown or not
}

When a user tries to register with an email address that has already been taken, the database will throw an Neo.ClientError.Schema.ConstraintViolation error. Instead of this being treated as an internal server error, it should instead be treated as a 422 Unprocessable Entity. This will allow the front end to handle this error appropriately.

A ValidationError class already exists which is handled by an Express middleware.

Completing the Challenge

To complete this challenge, you will first create a new constraint in your Sandbox database and modify the code to add a try/catch block.

Create a Unique Constraint

In order to ensure that a property and label combination is unique, you run a CREATE CONSTRAINT query. In this case, we need to ensure that the email property is unique across all nodes with a :User label.

Click the Run in Sandbox button to create the constraint on your Sandbox.

cypher
CREATE CONSTRAINT UserEmailUnique
IF NOT EXISTS
FOR (user:User)
REQUIRE user.email IS UNIQUE;

Add a Try/Catch Block

Open src/services/auth.service.js

In the register() method, you will need to:

  1. Try to create a User with the supplied email, password and name.

  2. Catch an error when it is thrown, using the code property to check and reformat a Neo.ClientError.Schema.ConstraintValidationFailed error into a ValidationError.

  3. Finally ensure that the session is closed regardless of whether the query is successful or an error is thrown.

Your code should look like this:

js
try {
  // Create the User node in a write transaction
  const res = await session.executeWrite(
    tx => tx.run(
      `
        CREATE (u:User {
          userId: randomUuid(),
          email: $email,
          password: $encrypted,
          name: $name
        })
        RETURN u
      `,
      { email, encrypted, name }
    )
  )

  // Extract the user from the result
  const [ first ] = res.records
  const node = first.get('u')

  const { password, ...safeProperties } = node.properties

  // Close the session
  await session.close()

  return {
    ...safeProperties,
    token: jwt.sign(this.userToClaims(safeProperties), JWT_SECRET),
  }
}
catch (e) {
  // Handle unique constraints in the database
  if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') {
    throw new ValidationError(
      `An account already exists with the email address ${email}`,
      {
        email: 'Email address already taken'
      }
    )
  }

  // Non-neo4j error
  throw e
}
finally {
  // Close the session
  await session.close()
}

Update the register() method to reflect the changes above, then scroll to Testing to verify that the code works as expected.

Working Solution

Click here to reveal the fully-implemented register() method.
js
async register(email, plainPassword, name) {
  const encrypted = await hash(plainPassword, parseInt(SALT_ROUNDS))

  // Open a new session
  const session = this.driver.session()

  try {
    // Create the User node in a write transaction
    const res = await session.executeWrite(
      tx => tx.run(
        `
          CREATE (u:User {
            userId: randomUuid(),
            email: $email,
            password: $encrypted,
            name: $name
          })
          RETURN u
        `,
        { email, encrypted, name }
      )
    )

    // Extract the user from the result
    const [ first ] = res.records
    const node = first.get('u')

    const { password, ...safeProperties } = node.properties

    // Close the session
    await session.close()

    return {
      ...safeProperties,
      token: jwt.sign(this.userToClaims(safeProperties), JWT_SECRET),
    }
  }
  catch (e) {
    // Handle unique constraints in the database
    if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') {
      throw new ValidationError(
        `An account already exists with the email address ${email}`,
        {
          email: 'Email address already taken'
        }
      )
    }

    // Non-neo4j error
    throw e
  }
  finally {
    // Close the session
    await session.close()
  }
}

Testing

To test that this functionality has been correctly implemented, run the following code in a new terminal session:

sh
Running the test
npm run test 04

The test file is located at test/challenges/04-handle-constraint-errors.spec.js.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 04-handle-constraint-errors branch by running:

sh
Check out the 04-handle-constraint-errors branch
git checkout 04-handle-constraint-errors

You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.

Verifying the Test

If you have completed the steps in this challenge, a Unique Constraint will have been added to the database.

Click the Check Database button below to verify the constraint has been correctly created.

Hint

Try running the Cypher statement at Create a Unique Constraint and then click Check Database again.

Solution

If you haven’t already done so, run the following statement to create the constraint:

cypher
CREATE CONSTRAINT UserEmailUnique
IF NOT EXISTS
FOR (user:User)
REQUIRE user.email IS UNIQUE;

The unit test then attempts to create a user twice with a random email address, with the test passing if the ValidationException error is thrown by the AuthService. Once you have run this statement, click Try again…​* to complete the challenge.

Lesson Summary

In this Challenge, you have modified the register() function to catch specific errors thrown by the database.

If you wanted to go further, you could use a Regular Expression to extract more specific information about the ConstraintValidationFailed error.

Now that a user is able to successfully register, in the next Challenge, you will update the authenticate() method to find our user in the database.