In the previous Challenge, we hardcoded a favorite
property on the return to the calls in the FavoriteService
.
We did this because we could guarantee the value based on the query being executed.
This won’t be possible in the MovieService
.
Your next challenge is to modify the MovieService
to dynamically determine whether a movie is on the current user’s My Favorite List.
This challenge has two parts:
-
Implement Multiple Transaction Calls
src/services/movie.service.js
→
Running Multiple Queries within a Transaction
So far, you have only used the tx
object within the Unit of Work to run a single query.
This will be fine for the majority of cases, but there may also be scenarios where more than one Query may be required.
User Favorites
One way that we could find the user’s favorites would be to run a separate MATCH
clause within the same query.
But as all of the routes that interact with the MovieService
are used by both anonymous and logged in users, this could add unwanted complexity to the service.
Instead, it would be cleaner to execute two separate queries within the same transaction; one query to fetch a list of the user’s favorites, and another to retrieve a list of movies.
Fortunately, with only a few minor tweaks to the code, we can create a
method that can be used to populate the favorite
flag in the every other method in the MovieService
.
Creating a Reusable Method
At the bottom of the MovieService
, a placeholder getUserFavorites()
method exists which is currently hardcoded to return an empty array.
async getUserFavorites(tx, userId) {
return []
}
The purpose of this function is to run a Cypher statement against the Transaction
object passed as the first parameter, which will find all of the user’s favorite movies and return a list of tmdbId
properties.
Your challenge is to modify this method to retrieve that list of Movie ID’s and then call this function from the Read Transaction in the all()
method.
Finding Favorites
Modify the getUserFavorites()
method to run the following query against the tx
parameter.
Click here to reveal the Cypher statement
This query should only run if a user is logged in for the current request, and therefore the userId
parameter is not undefined
.
MATCH (u:User {userId: $userId})-[:HAS_FAVORITE]->(m)
RETURN m.tmdbId AS id
Working Solution
Click here to reveal the completed getUserFavorites()
method
async getUserFavorites(tx, userId) {
// If userId is not defined, return an empty array
if ( userId === undefined ) {
return []
}
const favoriteResult = await tx.run(
`
MATCH (:User {userId: $userId})-[:HAS_FAVORITE]->(m)
RETURN m.tmdbId AS id
`,
{ userId, }
)
// Extract the `id` value returned by the cypher query
return favoriteResult.records.map(
row => row.get('id')
)
}
Multiple Transaction Calls
This part of the challenge requires you to run two queries within the same transaction,
The all()
method currently uses a function to immediately return the results of tx.run()
.
Instead of returning the results directly, you’ll need to update the query to be an async function which await
s the the results of a call to getUserFavorites(tx, userId)
, and then passes the results as a parameter to the existing query.
Working Solution
Click here to reveal the completed getUserFavorites()
method
async getUserFavorites(tx, userId) {
// If userId is not defined, return an empty array
if ( userId === undefined ) {
return []
}
const favoriteResult = await tx.run(
`
MATCH (:User {userId: $userId})-[:HAS_FAVORITE]->(m)
RETURN m.tmdbId AS id
`,
{ userId, }
)
// Extract the `id` value returned by the cypher query
return favoriteResult.records.map(
row => row.get('id')
)
}
Comparing Versions
If we take a look at the two versions of the all()
method, not much has changed.
The favorites
array has been passed through as a parameter to the query, and the query now uses the Cypher IN
clause to check if the ID is included in the array.
// Execute a query in a new Read Transaction
const res = await session.executeRead(
async tx => {
const favorites = await this.getUserFavorites(tx, userId)
return tx.run(
`
MATCH (m:Movie)
WHERE m.\`${sort}\` IS NOT NULL
RETURN m {
.*,
favorite: m.tmdbId IN $favorites
} AS movie
ORDER BY m.\`${sort}\` ${order}
SKIP $skip
LIMIT $limit
`, { skip: int(skip), limit: int(limit), favorites })
}
)
// Execute a query in a new Read Transaction
const res = await session.executeRead(
tx => tx.run(
`
MATCH (m:Movie)
WHERE m.\`${sort}\` IS NOT NULL
RETURN m {
.*
} AS movie
ORDER BY m.\`${sort}\` ${order}
SKIP $skip
LIMIT $limit
`, { skip: int(skip), limit: int(limit) })
)
Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
npm run test 08
The test file is located at test/challenges/08-favorite-flag.spec.js
.
Are you stuck? Click here for help
If you get stuck, you can see a working solution by checking out the 08-favorite-flag
branch by running:
git checkout 08-favorite-flag
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.flag@neo4j.com
will have added Band of Brothers, the most popular movie in the dataset to their list of favorites.
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.
MATCH (u:User {email: "graphacademy.flag@neo4j.com"})-[:HAS_FAVORITE]->(:Movie {title: 'Free Willy'})
RETURN true AS shouldVerify
Solution
The following statement will mimic the behaviour of the test by first finding the movie with the highest .imdbId
rating and merging a new :User
node into the graph with the email address graphacademy.flag@neo4j.com
.
The test then merges a :HAS_FAVORITE
relationship between the user and movie.
MATCH (m:Movie) WITH m
WHERE m.imdbRating IS NOT NULL
WITH m
ORDER BY m.imdbRating DESC LIMIT 1
MERGE (u:User {userId: '9f965bf6-7e32-4afb-893f-756f502b2c2a'})
SET u.email = 'graphacademy.favorite@neo4j.com'
MERGE (u)-[r:HAS_FAVORITE]->(m)
RETURN *
Once you have run this statement, click Try again…* to complete the challenge.
Module Summary
In this module, you have updated the project to read data from, and write data to Neo4j. You have also learned about some of the considerations that we need to make when working with the Cypher type system in our Node.js application.
In the Project Backlog module, you will continue to implement the remaining missing pieces of functionality. This isn’t strictly required, but will be good practice for your development journey ahead.