Rating Movies

The challenges in this course come thick and fast!

In this challenge, you will modify the add() method in the RatingService to save ratings into Neo4j. As part of the challenge, you will:

The Request Lifecycle

Rating a Movie

Before we start, let’s take a look at the request lifecycle when saving a review. If you prefer, you can skip to Saving a Rating.

On every Movie page, the user is invited to rate a movie on scale of 1 to 5. The form pictured to the right gives the user the ability to select a rating between 1 and 5 and click submit to save the rating.

When the form is submitted, the website sends a request to /api/account/ratings/{movieId} and the following will happen:

  1. The server directs the request to the route handler in src/routes/account.routes.js, which verifies the user’s JWT token before handling the request.

  2. The route handler creates an instance of the RatingService.

  3. The add() method is called on the RatingService, and is passed the ID of the current user plus the ID of the movie and a rating from the request body.

  4. It is then the responsibility of the add() method to save this information to the database and return an appropriate response.

A rating is represented in the graph a relationship going from a :User to a :Movie node with the type :RATED. The relationship has two properties; the rating (an integer) and a timestamp to represent when the relationship was created.

After the data is saved, the UI expects the movie details to be returned, with an additional property called rating, which will be the rating that the user has given for the movie.

Let’s take a look at the existing method in the RatingService.

js
src/services/rating.service.js
async add(userId, movieId, rating) {
  // TODO: Convert the native integer into a Neo4j Integer
  // TODO: Save the rating in the database
  // TODO: Return movie details and a rating

  return goodfellas
}

Your challenge is to replace the TODO comments in this method with working code.

Open src/services/rating.service.js

Saving a Rating

If you take the the comments one-by-one, you will need to convert the native integer into a Neo4j Integer, save the rating in the database and then return movie details and a rating.

Convert the Rating

To convert the native JavaScript integer to a Neo4j Integer, we can use the int() function exported from neo4j-driver.

js
import { int } from 'neo4j-driver'

// ...

// Convert the native integer into a Neo4j Integer
rating = int(rating)

We could also use the toInteger() function in Cypher to convert the value sent by the driver.

cypher
toInteger(20.0) // 20

Save the Rating in a Write Transaction

For this part of the challenge, open a new session, execute the query within a write transaction and close the session.

js
// Save the rating to the database

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

// Save the rating in the database
const res = await session.executeWrite(
  tx => tx.run(
    `
      MATCH (u:User {userId: $userId})
      MATCH (m:Movie {tmdbId: $movieId})
      MERGE (u)-[r:RATED]->(m)
      SET r.rating = $rating,
          r.timestamp = timestamp()
      RETURN m {
        .*,
        rating: r.rating
      } AS movie
    `,
    { userId, movieId, rating, }
  )
)

await session.close()

By using the MERGE keyword here, we will overwrite an existing rating if one already exists. This way we don’t need to worry about duplicates or deleting existing records.

If either the User or Movie could not be found, throw a NotFoundError.

js
// Check User and Movie exist
if ( res.records.length === 0 ) {
  throw new NotFoundError(
    `Could not create rating for Movie ${movieId} by User ${userId}`
  )
}

Return the Results

Finally, take the first row of the results and use the get() function to get the movie object returned by the query. Use the toNativeTypes() function to convert any non-native types into their JavaScript equivalent.

js
// Return movie details and rating
const [ first ] = res.records
const movie = first.get('movie')

return toNativeTypes(movie)

This example uses the Destructuring assignment technique to get the first record from the res.records array.

Working Solution

Click here to reveal the Working Solution.
js
src/services/rating.service.js
async add(userId, movieId, rating) {
  // Convert the native integer into a Neo4j Integer
  rating = int(rating)

  // Save the rating to the database

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

  // Save the rating in the database
  const res = await session.executeWrite(
    tx => tx.run(
      `
        MATCH (u:User {userId: $userId})
        MATCH (m:Movie {tmdbId: $movieId})
        MERGE (u)-[r:RATED]->(m)
        SET r.rating = $rating,
            r.timestamp = timestamp()
        RETURN m {
          .*,
          rating: r.rating
        } AS movie
      `,
      { userId, movieId, rating, }
    )
  )

  await session.close()

  // Check User and Movie exist
  if ( res.records.length === 0 ) {
    throw new NotFoundError(
      `Could not create rating for Movie ${movieId} by User ${userId}`
    )
  }

  // Return movie details and rating
  const [ first ] = res.records
  const movie = first.get('movie')

  return toNativeTypes(movie)
}

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 06

The test file is located at test/challenges/06-rating-movies.spec.js.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 06-rating-movies branch by running:

sh
Check out the 06-rating-movies branch
git checkout 06-rating-movies

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 the test has run successfully, a user with the email address graphacademy.reviewer@neo4j.com will have given the movie Goodfellas a rating of 5.

That number should have a type of INTEGER

Hint

You can run the following query to check for the user within the database. If the shouldVerify value returns true, the verification should be successful.

cypher
MATCH (u:User {email: "graphacademy.reviewer@neo4j.com"})-[r:RATED]->(m:Movie {title: "Goodfellas"})
RETURN u.email, m.title, r.rating,
    r.rating = 5 as shouldVerify

Solution

The following statement will mimic the behaviour of the test, merging a new :User node with the email address graphacademy.reviewer@neo4j.com and a :Movie node with a .tmdbId property of '769'. The test then merges a relationship between the user and movie nodes in the graph, giving the relationship a .rating property.

cypher
MERGE (u:User {userId: '1185150b-9e81-46a2-a1d3-eb649544b9c4'})
SET u.email = 'graphacademy.reviewer@neo4j.com'
MERGE (m:Movie {tmdbId: '769'})
MERGE (u)-[r:RATED]->(m)
SET r.rating = 5,
    r.timestamp = timestamp()
RETURN m {
    .*,
    rating: r.rating
} AS movie

Once you have run this statement, click Try again…​* to complete the challenge.

Lesson Summary

In this Challenge, you have updated the RatingService to save a relationship between a User and Movie to represent a Rating. You have also explicitly converted a JavaScript integer into a Neo4j Integer using the int() function.

In the next Challenge, you will implement the My Favorites feature.