├── .gitignore ├── src ├── main │ ├── resources │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── img │ │ │ │ ├── poster-placeholder.png │ │ │ │ ├── arrow2.9cb9648d.svg │ │ │ │ ├── checkmark.87d39455.svg │ │ │ │ ├── breadcrumb.31bd9686.svg │ │ │ │ ├── play.c4fc04ea.svg │ │ │ │ └── close.cb12eddc.svg │ │ │ ├── css │ │ │ │ ├── people.view.2872f0c5.css │ │ │ │ ├── movie.view.f3a542e6.css │ │ │ │ ├── register.d6a35621.css │ │ │ │ ├── people.list.12ebb465.css │ │ │ │ └── login.dfbbfbd9.css │ │ │ ├── js │ │ │ │ ├── about.7764984e.js │ │ │ │ ├── logout.9d1bec95.js │ │ │ │ ├── movies.latest.006f38b5.js │ │ │ │ ├── about.7764984e.js.map │ │ │ │ ├── logout.9d1bec95.js.map │ │ │ │ ├── movies.latest.006f38b5.js.map │ │ │ │ ├── favorites.ff43e072.js │ │ │ │ ├── genres.list.bb023af3.js │ │ │ │ ├── genres.view.78be501f.js │ │ │ │ ├── favorites.ff43e072.js.map │ │ │ │ ├── login.fb3010e6.js │ │ │ │ └── register.f0868032.js │ │ │ └── index.html │ │ ├── example.properties │ │ └── fixtures │ │ │ ├── users.json │ │ │ ├── ratings.cypher │ │ │ ├── genres.cypher │ │ │ ├── similar.cypher │ │ │ ├── popular.cypher │ │ │ ├── latest.cypher │ │ │ ├── pacino.json │ │ │ ├── ratings.json │ │ │ ├── shawshank.json │ │ │ ├── goodfellas.json │ │ │ ├── genres.json │ │ │ ├── similar.json │ │ │ ├── roles.json │ │ │ ├── pulpfiction.json │ │ │ ├── people.json │ │ │ └── comedy_movies.json │ └── java │ │ ├── neoflix │ │ ├── ValidationException.java │ │ ├── GsonUtils.java │ │ ├── Params.java │ │ ├── NeoflixApp.java │ │ ├── routes │ │ │ ├── GenreRoutes.java │ │ │ ├── AuthRoutes.java │ │ │ ├── PeopleRoutes.java │ │ │ ├── MovieRoutes.java │ │ │ └── AccountRoutes.java │ │ ├── AuthUtils.java │ │ ├── services │ │ │ ├── GenreService.java │ │ │ ├── RatingService.java │ │ │ ├── PeopleService.java │ │ │ ├── FavoriteService.java │ │ │ └── AuthService.java │ │ └── AppUtils.java │ │ └── example │ │ ├── CatchErrors.java │ │ ├── AsyncApi.java │ │ └── Index.java └── test │ └── java │ └── neoflix │ ├── _01_ConnectToNeo4jTest.java │ ├── _10_GenreDetailsTest.java │ ├── _09_GenreListTest.java │ ├── _06_RatingMoviesTest.java │ ├── _13_ListingRatingsTest.java │ ├── _15_PersonProfileTest.java │ ├── _12_MovieDetailsTest.java │ ├── _03_RegisterUserTest.java │ ├── _14_PersonListTest.java │ ├── _08_FavoriteFlagTest.java │ ├── _05_AuthenticationTest.java │ ├── _04_ConstraintErrorTest.java │ ├── _02_MovieListTest.java │ ├── _07_FavoritesListTest.java │ └── _11_MovieListTest.java ├── .gitmodules ├── diff ├── 01-connect-to-neo4j.diff ├── 04-handle-constraint-errors.diff ├── 14-person-list.diff ├── 09-genre-list.diff ├── 02-movie-lists.diff ├── 10-genre-details.diff ├── 06-rating-movies.diff ├── 08-favorite-flag.diff ├── 05-authentication.diff ├── 15-person-profile.diff ├── 03-registering-a-user.diff ├── 12-movie-details.diff ├── 13-listing-ratings.diff └── 11-movie-lists.diff ├── README.adoc └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | src/main/resources/application.properties 3 | node_modules 4 | .DS_Store 5 | .env 6 | target 7 | .idea 8 | -------------------------------------------------------------------------------- /src/main/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-graphacademy/app-java/HEAD/src/main/resources/public/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "neoflix-cypher"] 2 | path = neoflix-cypher 3 | url = https://github.com/neo4j-graphacademy/neoflix-cypher.git 4 | -------------------------------------------------------------------------------- /src/main/resources/example.properties: -------------------------------------------------------------------------------- 1 | APP_PORT=3000 2 | 3 | NEO4J_URI= 4 | NEO4J_USERNAME=neo4j 5 | NEO4J_PASSWORD= 6 | 7 | JWT_SECRET=secret 8 | SALT_ROUNDS=10 9 | -------------------------------------------------------------------------------- /src/main/resources/public/img/poster-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-graphacademy/app-java/HEAD/src/main/resources/public/img/poster-placeholder.png -------------------------------------------------------------------------------- /src/main/resources/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "userId": "1", 4 | "email": "graphacademy@neo4j.com", 5 | "password": "letmein", 6 | "name": "Graph Academy" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /src/main/resources/public/css/people.view.2872f0c5.css: -------------------------------------------------------------------------------- 1 | .article__content[data-v-56d6338a]{padding-top:80px;padding-bottom:60px}img[data-v-56d6338a]{display:block;width:100%;max-width:280px;border-radius:12px;margin-bottom:12px} -------------------------------------------------------------------------------- /src/main/resources/fixtures/ratings.cypher: -------------------------------------------------------------------------------- 1 | MATCH (m:Movie {title: "Goodfellas"})-[r:RATED]-(u:User) 2 | WITH { 3 | imdbRating: r.rating, timestamp: r.timestamp, 4 | user: u {tmdbId:u.userId, .name} 5 | } AS r 6 | ORDER BY r.timestamp DESC 7 | RETURN collect(r)[0..5] 8 | -------------------------------------------------------------------------------- /src/main/resources/public/img/arrow2.9cb9648d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/public/css/movie.view.f3a542e6.css: -------------------------------------------------------------------------------- 1 | .reviews__form[data-v-488525a2]{margin-bottom:24px}.card__extra{display:flex;flex-direction:row;justify-content:flex-start;width:100%;margin-top:16px;font-size:12px}.card__extra svg{margin-right:6px}.card__extra svg *{stroke:#e0e0e0} -------------------------------------------------------------------------------- /src/main/resources/public/css/register.d6a35621.css: -------------------------------------------------------------------------------- 1 | .sign{position:fixed;inset:0;background:#151f30;z-index:2000}.sign__content svg{margin-bottom:32px}.error-message{padding:17px;flex-wrap:wrap;border:1px solid #eb5757;border-radius:12px;font-size:12px}.sign__input.error{border-color:#eb5757} -------------------------------------------------------------------------------- /src/main/resources/public/css/people.list.12ebb465.css: -------------------------------------------------------------------------------- 1 | .catalog__search{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;background-color:#131720;border-radius:16px;position:relative;width:100%;z-index:2;margin-right:24px}.catalog__search input{background:transparent} -------------------------------------------------------------------------------- /src/main/resources/public/img/checkmark.87d39455.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/public/css/login.dfbbfbd9.css: -------------------------------------------------------------------------------- 1 | .sign{position:fixed;inset:0;background:#151f30;z-index:2000}.sign__content svg{margin-bottom:32px}.error-message{padding:17px;flex-wrap:wrap;border:1px solid #eb5757;border-radius:12px;font-size:12px}.sign__input.error{border-color:#eb5757}.error[data-v-109c238d]{padding:1rem;flex-wrap:wrap} -------------------------------------------------------------------------------- /src/main/resources/public/img/breadcrumb.31bd9686.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/public/js/about.7764984e.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["about"],{f820:function(n,t,a){"use strict";a.r(t);var e=a("7a23"),u={class:"about"},c=Object(e["g"])("h1",null,"This is an about page",-1),o=[c];function s(n,t){return Object(e["u"])(),Object(e["f"])("div",u,o)}var b=a("6b0d"),i=a.n(b);const r={},d=i()(r,[["render",s]]);t["default"]=d}}]); 2 | //# sourceMappingURL=about.7764984e.js.map -------------------------------------------------------------------------------- /src/main/resources/fixtures/genres.cypher: -------------------------------------------------------------------------------- 1 | MATCH (g:Genre)<-[:IN_GENRE]-(m:Movie) 2 | WHERE m.imdbRating IS NOT NULL AND m.poster IS NOT NULL AND g.name <> '(no genres listed)' 3 | WITH g, m 4 | ORDER BY m.imdbRating DESC 5 | 6 | WITH g, collect(m)[0] AS movie 7 | 8 | RETURN g { 9 | link: '/genres/'+ g.name, 10 | .name, 11 | movies: size((g)<-[:IN_GENRE]-()), 12 | poster: movie.poster 13 | } AS genre 14 | ORDER BY g.name ASC 15 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/similar.cypher: -------------------------------------------------------------------------------- 1 | MATCH (:Movie {title: "Goodfellas"})<-[r:RATED]-(u:User)-[r2:RATED]->(n:Movie) 2 | 3 | WHERE r.rating > 4.0 AND r2.rating >= r.rating 4 | 5 | RETURN n { 6 | tmdbId:n.imdbId, 7 | .poster, 8 | .title, 9 | .year, 10 | .languages, 11 | .plot, 12 | imdbRating: n.imdbRating, 13 | genres: [ (n)-[:IN_GENRE]->(g) | g {link: '/genres/'+ g.name, .name}] 14 | } AS movie 15 | 16 | , avg(r2.rating) AS rating ORDER BY rating DESC LIMIT 5 -------------------------------------------------------------------------------- /src/main/java/neoflix/ValidationException.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import java.util.*; 4 | 5 | public class ValidationException extends RuntimeException { 6 | private final Map details = new HashMap<>(); 7 | public ValidationException(String message, Map details) { 8 | super(message); 9 | if (details!=null) this.details.putAll(details); 10 | } 11 | public Map getDetails() { 12 | return details; 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/resources/public/js/logout.9d1bec95.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["logout"],{c100:function(n,t,e){"use strict";e.r(t);var c=e("7a23");function o(n,t,e,o,u,a){return Object(c["u"])(),Object(c["f"])("div",null,"Please wait…")}var u=e("d617"),a=e("6c02"),i=Object(c["k"])({setup:function(){var n=Object(u["a"])(),t=n.logout,e=Object(a["d"])(),c=e.push;t().then((function(){c({name:"Home"})}))}}),r=e("6b0d"),s=e.n(r);const b=s()(i,[["render",o]]);t["default"]=b}}]); 2 | //# sourceMappingURL=logout.9d1bec95.js.map -------------------------------------------------------------------------------- /src/main/resources/fixtures/popular.cypher: -------------------------------------------------------------------------------- 1 | MATCH (n:Movie) 2 | 3 | WHERE n.imdbRating IS NOT NULL and n.poster IS NOT NULL 4 | 5 | WITH n { 6 | tmdbId, 7 | .poster, 8 | .title, 9 | .year, 10 | .languages, 11 | .plot, 12 | imdbRating: n.imdbRating, 13 | directors: [ (n)<-[:DIRECTED]-(d) | d { tmdbId:d.imdbId, .name } ], 14 | actors: [ (n)<-[:ACTED_IN]-(p) | p { tmdbId:p.imdbId, .name } ][0..5], 15 | genres: [ (n)-[:IN_GENRE]->(g) | g {link: '/genres/'+ g.name, .name}] 16 | } 17 | ORDER BY n.rating DESC 18 | LIMIT 6 19 | RETURN collect(n); -------------------------------------------------------------------------------- /src/main/resources/fixtures/latest.cypher: -------------------------------------------------------------------------------- 1 | MATCH (n:Movie) 2 | 3 | WHERE n.released IS NOT NULL and n.poster IS NOT NULL 4 | 5 | WITH n { 6 | .tmdbId, 7 | .poster, 8 | .title, 9 | .year, 10 | .languages, 11 | .plot, 12 | imdbRating: n.imdbRating, 13 | directors: [ (n)<-[:DIRECTED]-(d) | d { tmdbId:d.imdbId, .name } ], 14 | actors: [ (n)<-[:ACTED_IN]-(p) | p { tmdbId:p.imdbId, .name } ][0..5], 15 | genres: [ (n)-[:IN_GENRE]->(g) | g {link: '/genres/'+ g.name, .name}] 16 | } 17 | ORDER BY n.released DESC 18 | LIMIT 6 19 | RETURN collect(n) 20 | -------------------------------------------------------------------------------- /diff/01-connect-to-neo4j.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/AppUtils.java b/src/main/java/neoflix/AppUtils.java 2 | index e0e6440..e26eb0b 100644 3 | --- a/src/main/java/neoflix/AppUtils.java 4 | +++ b/src/main/java/neoflix/AppUtils.java 5 | @@ -45,8 +45,10 @@ public class AppUtils { 6 | 7 | // tag::initDriver[] 8 | static Driver initDriver() { 9 | - // TODO: Create and assign an instance of the driver here 10 | - return null; 11 | + AuthToken auth = AuthTokens.basic(getNeo4jUsername(), getNeo4jPassword()); 12 | + Driver driver = GraphDatabase.driver(getNeo4jUri(), auth); 13 | + driver.verifyConnectivity(); 14 | + return driver; 15 | } 16 | // end::initDriver[] 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/pacino.json: -------------------------------------------------------------------------------- 1 | { 2 | "bornIn": "New York City, New York, USA", 3 | "tmdbId": "1158", 4 | "id": "0000199", 5 | "born": "1940-04-25", 6 | "name": "Al Pacino", 7 | "bio": "Alfredo James \"Al\" Pacino (born April 25, 1940) is an American film and stage actor and director. He is famous for playing mobsters, including Michael Corleone in The Godfather trilogy, Tony Montana in Scarface, Alphonse \"Big Boy\" Caprice in Dick Tracy and Carlito Brigante in Carlito's Way, though he has also appeared several times on the other side of the law — as a police officer, detective and a lawyer...", 8 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/sLsw9Dtj4mkL8aPmCrh38Ap9Xhq.jpg", 9 | "url": "https://themoviedb.org/person/1158", 10 | "directedCount": 2, 11 | "actedCount": 3 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/public/js/movies.latest.006f38b5.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["movies.latest"],{"1a3b":function(e,t,r){"use strict";r.r(t);r("4e82");var o=r("7a23");function s(e,t,r,s,a,c){var i=Object(o["C"])("movie-grid");return Object(o["u"])(),Object(o["d"])(i,{title:e.title,to:e.path,sort:e.sort,order:e.order,showLoadMore:!0},null,8,["title","to","sort","order"])}var a=r("6c02"),c=r("e490"),i=r("5e73"),n=r("9bc1"),b=Object(o["k"])({components:{MovieGrid:n["a"]},setup:function(){var e=Object(a["c"])(),t=e.path.substr(1),r="Movies",o=c["c"];switch(t){case"popular":r="Popular Movies",o=c["b"];break;case"latest":r="Latest Releases",o=c["c"];break}return{title:r,path:t,sort:o,order:i["a"]}}}),u=r("6b0d"),d=r.n(u);const p=d()(b,[["render",s]]);t["default"]=p}}]); 2 | //# sourceMappingURL=movies.latest.006f38b5.js.map -------------------------------------------------------------------------------- /src/main/resources/fixtures/ratings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "rating": 2.0, 4 | "user": { 5 | "name": "Catherine Trujillo", 6 | "id": "570" 7 | }, 8 | "timestamp": 1475784311 9 | }, 10 | { 11 | "rating": 5.0, 12 | "user": { 13 | "name": "Teresa Graham", 14 | "id": "457" 15 | }, 16 | "timestamp": 1471383372 17 | }, 18 | { 19 | "rating": 5.0, 20 | "user": { 21 | "name": "Meredith Leonard", 22 | "id": "519" 23 | }, 24 | "timestamp": 1471150621 25 | }, 26 | { 27 | "rating": 4.0, 28 | "user": { 29 | "name": "Dr. Angela Johnson", 30 | "id": "56" 31 | }, 32 | "timestamp": 1467003139 33 | }, 34 | { 35 | "rating": 5.0, 36 | "user": { 37 | "name": "Melissa King", 38 | "id": "483" 39 | }, 40 | "timestamp": 1465387394 41 | } 42 | ] -------------------------------------------------------------------------------- /src/main/resources/public/js/about.7764984e.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///js/about.f2f4a2c3.js"],"names":["window","push","f820","module","__webpack_exports__","__webpack_require__","r","vue_runtime_esm_bundler","_hoisted_1","class","_hoisted_2","Object","_hoisted_3","render","_ctx","_cache","exportHelper","exportHelper_default","n","script","__exports__"],"mappings":"CAACA,OAAO,gBAAkBA,OAAO,iBAAmB,IAAIC,KAAK,CAAC,CAAC,SAAS,CAElEC,KACA,SAAUC,EAAQC,EAAqBC,GAE7C,aAEAA,EAAoBC,EAAEF,GAGtB,IAAIG,EAA0BF,EAAoB,QAI9CG,EAAa,CACfC,MAAO,SAGLC,EAA0BC,OAAOJ,EAAwB,KAA/BI,CAA8D,KAAM,KAAM,yBAA0B,GAE9HC,EAAa,CAACF,GAClB,SAASG,EAAOC,EAAMC,GACpB,OAAOJ,OAAOJ,EAAwB,KAA/BI,GAAwDA,OAAOJ,EAAwB,KAA/BI,CAA8D,MAAOH,EAAYI,GAKlJ,IAAII,EAAeX,EAAoB,QACnCY,EAAoCZ,EAAoBa,EAAEF,GAI9D,MAAMG,EAAS,GAGTC,EAA2BH,IAAuBE,EAAQ,CAAC,CAAC,SAASN,KAElCT,EAAoB,WAAa","file":"js/about.7764984e.js","sourceRoot":""} -------------------------------------------------------------------------------- /src/test/java/neoflix/_01_ConnectToNeo4jTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import org.junit.jupiter.api.Assumptions; 4 | import org.junit.jupiter.api.Test; 5 | import org.neo4j.driver.Driver; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 8 | import static org.junit.jupiter.api.Assertions.assertNotNull; 9 | 10 | class _01_ConnectToNeo4jTest { 11 | 12 | @Test 13 | void createDriverAndConnectToServer() { 14 | AppUtils.loadProperties(); 15 | assertNotNull(AppUtils.getNeo4jUri(), "neo4j uri defined"); 16 | assertNotNull(AppUtils.getNeo4jUsername(), "username defined"); 17 | assertNotNull(AppUtils.getNeo4jPassword(), "password defined"); 18 | 19 | Driver driver = AppUtils.initDriver(); 20 | Assumptions.assumeTrue(driver != null); 21 | assertNotNull(driver, "driver instantiated"); 22 | assertDoesNotThrow(driver::verifyConnectivity,"unable to verify connectivity"); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/resources/public/img/play.c4fc04ea.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/public/js/logout.9d1bec95.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///js/logout.c47f5258.js"],"names":["window","push","c100","module","__webpack_exports__","__webpack_require__","r","vue_runtime_esm_bundler","render","_ctx","_cache","$props","$setup","$data","$options","Object","auth","vue_router_esm_bundler","Logoutvue_type_script_lang_ts","setup","_useAuth","logout","_useRouter","then","name","exportHelper","exportHelper_default","n","__exports__"],"mappings":"CAACA,OAAO,gBAAkBA,OAAO,iBAAmB,IAAIC,KAAK,CAAC,CAAC,UAAU,CAEnEC,KACA,SAAUC,EAAQC,EAAqBC,GAE7C,aAEAA,EAAoBC,EAAEF,GAGtB,IAAIG,EAA0BF,EAAoB,QAIlD,SAASG,EAAOC,EAAMC,EAAQC,EAAQC,EAAQC,EAAOC,GACnD,OAAOC,OAAOR,EAAwB,KAA/BQ,GAAwDA,OAAOR,EAAwB,KAA/BQ,CAA8D,MAAO,KAAM,gBAK5I,IAAIC,EAAOX,EAAoB,QAG3BY,EAAyBZ,EAAoB,QAMhBa,EAAiCH,OAAOR,EAAwB,KAAhC,CAA4D,CAC3HY,MAAO,WACL,IAAIC,EAAWL,OAAOC,EAAK,KAAZD,GACXM,EAASD,EAASC,OAElBC,EAAaP,OAAOE,EAAuB,KAA9BF,GACbd,EAAOqB,EAAWrB,KAEtBoB,IAASE,MAAK,WACZtB,EAAK,CACHuB,KAAM,eAQVC,EAAepB,EAAoB,QACnCqB,EAAoCrB,EAAoBsB,EAAEF,GAQ9D,MAAMG,EAA2BF,IAAuBR,EAA+B,CAAC,CAAC,SAASV,KAExDJ,EAAoB,WAAa","file":"js/logout.9d1bec95.js","sourceRoot":""} -------------------------------------------------------------------------------- /src/main/resources/fixtures/shawshank.json: -------------------------------------------------------------------------------- 1 | { 2 | "actors": [ 3 | { 4 | "name": "Tim Robbins", 5 | "tmdbId": "0000209" 6 | }, 7 | { 8 | "name": "William Sadler", 9 | "tmdbId": "0006669" 10 | }, 11 | { 12 | "name": "Bob Gunton", 13 | "tmdbId": "0348409" 14 | }, 15 | { 16 | "name": "Morgan Freeman", 17 | "tmdbId": "0000151" 18 | } 19 | ], 20 | "languages": [ 21 | "English" 22 | ], 23 | "plot": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", 24 | "year": 1994, 25 | "genres": [ 26 | { 27 | "link": "/genres/Drama", 28 | "name": "Drama" 29 | }, 30 | { 31 | "link": "/genres/Crime", 32 | "name": "Crime" 33 | } 34 | ], 35 | "directors": [ 36 | { 37 | "name": "Frank Darabont", 38 | "tmdbId": "0001104" 39 | } 40 | ], 41 | "imdbRating": 9.3, 42 | "tmdbId": "0111161", 43 | "title": "Shawshank Redemption, The", 44 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/5KCVkau1HEl7ZzfPsKAPM0sMiKc.jpg" 45 | } -------------------------------------------------------------------------------- /diff/04-handle-constraint-errors.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/AuthService.java b/src/main/java/neoflix/services/AuthService.java 2 | index d8a5487..8c83d07 100644 3 | --- a/src/main/java/neoflix/services/AuthService.java 4 | +++ b/src/main/java/neoflix/services/AuthService.java 5 | @@ -5,6 +5,7 @@ import neoflix.AuthUtils; 6 | import neoflix.ValidationException; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Values; 9 | +import org.neo4j.driver.exceptions.ClientException; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | @@ -73,7 +74,15 @@ public class AuthService { 14 | // tag::return-register[] 15 | return userWithToken(user, token); 16 | // end::return-register[] 17 | + // tag::catch[] 18 | + } catch (ClientException e) { 19 | + // Handle unique constraints in the database 20 | + if (e.code().equals("Neo.ClientError.Schema.ConstraintValidationFailed")) { 21 | + throw new ValidationException("An account already exists with the email address", Map.of("email","Email address already taken")); 22 | + } 23 | + throw e; 24 | } 25 | + // end::catch[] 26 | } 27 | // end::register[] 28 | 29 | -------------------------------------------------------------------------------- /src/main/resources/public/img/close.cb12eddc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_10_GenreDetailsTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.GenreService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | class _10_GenreDetailsTest { 13 | private static Driver driver; 14 | 15 | @BeforeAll 16 | static void initDriver() { 17 | AppUtils.loadProperties(); 18 | driver = AppUtils.initDriver(); 19 | } 20 | 21 | @AfterAll 22 | static void closeDriver() { 23 | if (driver != null) driver.close(); 24 | } 25 | 26 | @Test 27 | void getGenreDetails() { 28 | GenreService genreService = new GenreService(driver); 29 | 30 | var genreName = "Action"; 31 | 32 | var output = genreService.find(genreName); 33 | assertNotNull(output); 34 | assertEquals(genreName, output.get("name")); 35 | 36 | System.out.println(""" 37 | 38 | Here is the answer to the quiz question on the lesson: 39 | How many movies are in the Action genre? 40 | Copy and paste the following answer into the text box: 41 | """); 42 | System.out.println(output.get("movies")); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Building Neo4j Applications with Java 2 | 3 | > Learn how to interact with Neo4j from Java using the Neo4j Java Driver 4 | 5 | This repository accompanies the link:https://graphacademy.neo4j.com/courses/app-java/[Building Neo4j Applications with Java course^] on link:https://graphacademy.neo4j.com/[Neo4j GraphAcademy^]. 6 | 7 | For a complete walkthrough of this repository, link:https://graphacademy.neo4j.com/courses/app-java/[enroll now^]. 8 | 9 | == Setup 10 | 11 | . Clone repository 12 | . Install https://sdkman.io[sdkman^] to manage JDK and Maven 13 | 14 | ---- 15 | sdk install java 17-open 16 | sdk use java 17-open 17 | sdk install maven 18 | mvn verify 19 | mvn compile exec:java 20 | ---- 21 | 22 | .Connection details to your neo4j database are in `src/main/resources/application.properties` 23 | [source,properties] 24 | ---- 25 | APP_PORT=3000 26 | 27 | NEO4J_URI=bolt://hostname-or-ip:7687 28 | NEO4J_USERNAME=neo4j 29 | NEO4J_PASSWORD= 30 | 31 | JWT_SECRET=secret 32 | SALT_ROUNDS=10 33 | ---- 34 | 35 | == A Note on comments 36 | 37 | You may spot a number of comments in this repository that look a little like this: 38 | 39 | [source,java] 40 | ---- 41 | // tag::something[] 42 | someCode() 43 | // end::something[] 44 | ---- 45 | 46 | 47 | We use link:https://asciidoc-py.github.io/index.html[Asciidoc^] to author our courses. 48 | Using these tags means that we can use a macro to include portions of code directly into the course itself. 49 | 50 | From the point of view of the course, you can go ahead and ignore them. -------------------------------------------------------------------------------- /src/main/java/example/CatchErrors.java: -------------------------------------------------------------------------------- 1 | package example; 2 | // tag::import[] 3 | // Import all relevant classes from neo4j-java-driver dependency 4 | import org.neo4j.driver.*; 5 | import org.neo4j.driver.exceptions.*; 6 | // end::import[] 7 | 8 | import neoflix.ValidationException; 9 | 10 | import java.util.*; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.logging.Level; 13 | 14 | public class CatchErrors { 15 | 16 | static String username = "neo4j"; 17 | static String password = "letmein!"; 18 | 19 | static Driver driver = GraphDatabase.driver("neo4j://localhost:7687", 20 | AuthTokens.basic(username, password)); 21 | 22 | 23 | public static void main () { 24 | String email = "uniqueconstraint@example.com"; 25 | // tag::constraint-error[] 26 | try (var session = driver.session()) { 27 | session.writeTransaction(tx -> { 28 | var res = tx.run("CREATE (u:User {email: $email}) RETURN u", 29 | Values.parameters("email", email)); 30 | return res.single().get('u').asMap(); 31 | }); 32 | } catch(Neo4jException e) { 33 | if (e.code().equals("Neo.ClientError.Schema.ConstraintValidationFailed")) { 34 | // System.err.println(e.getMessage()); // Node(33880) already exists with... 35 | throw new ValidationException("An account already exists with the email address "+email, 36 | Map.of("email","Email address already taken")); 37 | } 38 | throw e; 39 | } 40 | // end::constraint-error[] 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | NeoflixWe're sorry but Neoflix doesn't work properly without JavaScript enabled. Please enable it to continue. -------------------------------------------------------------------------------- /src/main/java/neoflix/GsonUtils.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import com.google.gson.*; 4 | 5 | import java.lang.reflect.Type; 6 | import java.time.LocalDate; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.List; 9 | 10 | public class GsonUtils { 11 | public static Gson gson() { 12 | try { 13 | Class type = Class.forName("java.util.Collections$EmptyList"); 14 | GsonBuilder gsonBuilder = new GsonBuilder() 15 | .registerTypeAdapter(LocalDate.class, new LocalDateSerializer()) 16 | .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) 17 | .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) 18 | .registerTypeAdapter(type, new EmptyListSerializer()); 19 | return gsonBuilder.create(); 20 | } catch(ClassNotFoundException cnfe) { 21 | throw new RuntimeException(cnfe); 22 | } 23 | } 24 | 25 | static class LocalDateSerializer implements JsonSerializer { 26 | private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy"); 27 | 28 | @Override 29 | public JsonElement serialize(LocalDate localDate, Type srcType, JsonSerializationContext context) { 30 | return new JsonPrimitive(formatter.format(localDate)); 31 | } 32 | } 33 | static class EmptyListSerializer implements JsonSerializer { 34 | 35 | @Override 36 | public JsonElement serialize(List list, Type srcType, JsonSerializationContext context) { 37 | return new JsonArray(0); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_09_GenreListTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.GenreService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | 9 | import java.util.Comparator; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertNotNull; 13 | 14 | class _09_GenreListTest { 15 | private static Driver driver; 16 | 17 | @BeforeAll 18 | static void initDriver() { 19 | AppUtils.loadProperties(); 20 | driver = AppUtils.initDriver(); 21 | } 22 | 23 | @AfterAll 24 | static void closeDriver() { 25 | if (driver != null) driver.close(); 26 | } 27 | 28 | @Test 29 | void getGenreList() { 30 | GenreService genreService = new GenreService(driver); 31 | 32 | var output = genreService.all(); 33 | assertNotNull(output); 34 | assertEquals(19, output.size()); 35 | assertEquals("Action", output.get(0).get("name")); 36 | assertEquals("Western", output.get(18).get("name")); 37 | 38 | output.sort(Comparator.comparing(m -> Integer.parseInt(m.get("movies").toString()), Comparator.nullsLast(Comparator.reverseOrder()))); 39 | 40 | System.out.println(""" 41 | 42 | Here is the answer to the quiz question on the lesson: 43 | Which genre has the highest movie count? 44 | Copy and paste the following answer into the text box: 45 | """); 46 | System.out.println(output.get(0).get("name")); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_06_RatingMoviesTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.RatingService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Values; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | 13 | class _06_RatingMoviesTest { 14 | private static Driver driver; 15 | 16 | private static final String email = "graphacademy.reviewer@neo4j.com"; 17 | private static final String movieId = "680"; 18 | private static final String userId = "1185150b-9e81-46a2-a1d3-eb649544b9c4"; 19 | private static final int rating = 5; 20 | 21 | @BeforeAll 22 | static void initDriver() { 23 | AppUtils.loadProperties(); 24 | driver = AppUtils.initDriver(); 25 | 26 | if (driver != null) driver.session().executeWrite(tx -> tx.run(""" 27 | MERGE (u:User {userId: $userId}) SET u.email = $email 28 | """, Values.parameters("userId", userId, "email", email))); 29 | } 30 | 31 | @AfterAll 32 | static void closeDriver() { 33 | if (driver != null) driver.close(); 34 | } 35 | 36 | @Test 37 | void writeMovieRatingAsInt() { 38 | RatingService ratingService = new RatingService(driver); 39 | 40 | var output = ratingService.add(userId, movieId, rating); 41 | 42 | assertNotNull(output); 43 | assertEquals(movieId, output.get("tmdbId")); 44 | assertEquals(rating, Integer.parseInt(output.get("rating").toString())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /diff/14-person-list.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/PeopleService.java b/src/main/java/neoflix/services/PeopleService.java 2 | index 6fcee1f..a3970eb 100644 3 | --- a/src/main/java/neoflix/services/PeopleService.java 4 | +++ b/src/main/java/neoflix/services/PeopleService.java 5 | @@ -38,14 +38,28 @@ public class PeopleService { 6 | */ 7 | // tag::all[] 8 | public List> all(Params params) { 9 | - // TODO: Get a list of people from the database 10 | - if (params.query() != null) { 11 | - return AppUtils.process(people.stream() 12 | - .filter(p -> ((String)p.get("name")) 13 | - .contains(params.query())) 14 | - .toList(), params); 15 | + // Open a new database session 16 | + try (var session = driver.session()) { 17 | + // Get a list of people from the database 18 | + var res = session.executeRead(tx -> { 19 | + String statement = String.format(""" 20 | + MATCH (p:Person) 21 | + WHERE $q IS null OR p.name CONTAINS $q 22 | + RETURN p { .* } AS person 23 | + ORDER BY p.`%s` %s 24 | + SKIP $skip 25 | + LIMIT $limit 26 | + """, params.sort(Params.Sort.name), params.order()); 27 | + return tx.run(statement 28 | + , Values.parameters("q", params.query(), "skip", params.skip(), "limit", params.limit())) 29 | + .list(row -> row.get("person").asMap()); 30 | + }); 31 | + 32 | + return res; 33 | + } catch(Exception e) { 34 | + e.printStackTrace(); 35 | } 36 | - return AppUtils.process(people, params); 37 | + return List.of(); 38 | } 39 | // end::all[] 40 | 41 | -------------------------------------------------------------------------------- /diff/09-genre-list.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/GenreService.java b/src/main/java/neoflix/services/GenreService.java 2 | index fcac519..5a6fc96 100644 3 | --- a/src/main/java/neoflix/services/GenreService.java 4 | +++ b/src/main/java/neoflix/services/GenreService.java 5 | @@ -34,11 +34,36 @@ public class GenreService { 6 | */ 7 | // tag::all[] 8 | public List> all() { 9 | - // TODO: Open a new session 10 | - // TODO: Get a list of Genres from the database 11 | - // TODO: Close the session 12 | + // Open a new Session, close automatically at the end 13 | + try (var session = driver.session()) { 14 | + // Get a list of Genres from the database 15 | + var query = """ 16 | + MATCH (g:Genre) 17 | + WHERE g.name <> '(no genres listed)' 18 | + CALL { 19 | + WITH g 20 | + MATCH (g)<-[:IN_GENRE]-(m:Movie) 21 | + WHERE m.imdbRating IS NOT NULL 22 | + AND m.poster IS NOT NULL 23 | + RETURN m.poster AS poster 24 | + ORDER BY m.imdbRating DESC LIMIT 1 25 | + } 26 | + RETURN g { 27 | + .name, 28 | + link: '/genres/'+ g.name, 29 | + poster: poster, 30 | + movies: size( (g)<-[:IN_GENRE]-() ) 31 | + } as genre 32 | + ORDER BY g.name ASC 33 | + """; 34 | + var genres = session.executeRead( 35 | + tx -> tx.run(query) 36 | + .list(row -> 37 | + row.get("genre").asMap())); 38 | 39 | - return genres; 40 | + // Return results 41 | + return genres; 42 | + } 43 | } 44 | // end::all[] 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/neoflix/Params.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import static neoflix.Params.Sort.*; 4 | 5 | import io.javalin.http.Context; 6 | 7 | import java.util.EnumSet; 8 | import java.util.Optional; 9 | 10 | public record Params(String query, Sort sort, Order order, int limit, int skip) { 11 | public Sort sort(Sort defaultSort) { 12 | return sort == null ? defaultSort : sort; 13 | } 14 | 15 | public enum Order { 16 | ASC, DESC; 17 | 18 | static Order of(String value) { 19 | if (value == null || value.isBlank() || !"DESC".equalsIgnoreCase(value)) return ASC; 20 | return DESC; 21 | } 22 | } 23 | 24 | public enum Sort { /* Movie */ 25 | title, released, imdbRating, score, 26 | /* Person */ name, born, movieCount, 27 | /* */ rating, timestamp; 28 | 29 | static Sort of(String name) { 30 | if (name == null || name.isBlank()) return null; 31 | return Sort.valueOf(name); 32 | } 33 | } 34 | 35 | public static final EnumSet MOVIE_SORT = EnumSet.of(title, released, imdbRating, score); 36 | public static final EnumSet PEOPLE_SORT = EnumSet.of(name, born, movieCount); 37 | public static final EnumSet RATING_SORT = EnumSet.of(rating, timestamp); 38 | 39 | public static Params parse(Context ctx, EnumSet validSort) { 40 | String q = ctx.queryParam("q"); 41 | Sort sort = Sort.of(ctx.queryParam("sort")); 42 | Order order = Order.of(ctx.queryParam("order")); 43 | int limit = Integer.parseInt(Optional.ofNullable(ctx.queryParam("limit")).orElse("6")); 44 | int skip = Integer.parseInt(Optional.ofNullable(ctx.queryParam("skip")).orElse("0")); 45 | // Only accept valid sort fields 46 | if (!validSort.contains(sort)) { 47 | sort = validSort.iterator().next(); 48 | } 49 | return new Params(q, sort, order, limit, skip); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/neoflix/NeoflixApp.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import java.util.*; 4 | 5 | import io.javalin.Javalin; 6 | import io.javalin.http.staticfiles.Location; 7 | import neoflix.routes.*; 8 | 9 | import static io.javalin.apibuilder.ApiBuilder.path; 10 | 11 | public class NeoflixApp { 12 | 13 | public static void main(String[] args) { 14 | AppUtils.loadProperties(); 15 | 16 | // tag::driver[] 17 | var driver = AppUtils.initDriver(); 18 | // end::driver[] 19 | 20 | var jwtSecret = AppUtils.getJwtSecret(); 21 | var port = AppUtils.getServerPort(); 22 | 23 | var gson = GsonUtils.gson(); 24 | var server = Javalin 25 | .create(config -> { 26 | config.addStaticFiles("/", Location.CLASSPATH); 27 | config.addStaticFiles(staticFiles -> { 28 | staticFiles.hostedPath = "/"; 29 | staticFiles.directory = "/public"; 30 | staticFiles.location = Location.CLASSPATH; 31 | }); 32 | }) 33 | .before(ctx -> AppUtils.handleAuthAndSetUser(ctx.req, jwtSecret)) 34 | .routes(() -> { 35 | path("/api", () -> { 36 | path("/movies", new MovieRoutes(driver, gson)); 37 | path("/genres", new GenreRoutes(driver, gson)); 38 | path("/auth", new AuthRoutes(driver, gson, jwtSecret)); 39 | path("/account", new AccountRoutes(driver, gson)); 40 | path("/people", new PeopleRoutes(driver, gson)); 41 | }); 42 | }) 43 | .exception(ValidationException.class, (exception, ctx) -> { 44 | var body = Map.of("message", exception.getMessage(), "details", exception.getDetails()); 45 | ctx.status(422).contentType("application/json").result(gson.toJson(body)); 46 | }) 47 | .start(port); 48 | System.out.printf("Server listening on http://localhost:%d/%n", port); 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/java/neoflix/routes/GenreRoutes.java: -------------------------------------------------------------------------------- 1 | package neoflix.routes; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import io.javalin.apibuilder.EndpointGroup; 6 | import neoflix.Params; 7 | import neoflix.AppUtils; 8 | import neoflix.services.GenreService; 9 | import neoflix.services.MovieService; 10 | import org.neo4j.driver.Driver; 11 | 12 | import static io.javalin.apibuilder.ApiBuilder.get; 13 | 14 | public class GenreRoutes implements EndpointGroup { 15 | private final Gson gson; 16 | private final GenreService genreService; 17 | private final MovieService movieService; 18 | 19 | public GenreRoutes(Driver driver, Gson gson) { 20 | genreService = new GenreService(driver); // new GenreServiceFixture(); 21 | movieService = new MovieService(driver); 22 | this.gson = gson; 23 | } 24 | 25 | @Override 26 | public void addEndpoints() { 27 | /* 28 | * @GET /genres/ 29 | * 30 | * This route should retrieve a full list of Genres from the 31 | * database along with a poster and movie count. 32 | */ 33 | get("", ctx -> ctx.result(gson.toJson(genreService.all()))); 34 | 35 | /* 36 | * @GET /genres/{name} 37 | * 38 | * This route should return information on a genre with a name 39 | * that matches the {name} URL parameter. If the genre is not found, 40 | * a 404 should be thrown. 41 | */ 42 | get("/{name}", ctx -> ctx.result(gson.toJson(genreService.find(ctx.pathParam("name"))))); 43 | 44 | /** 45 | * @GET /genres/{name}/movies 46 | * 47 | * This route should return a paginated list of movies that are listed in 48 | * the genre whose name matches the {name} URL parameter. 49 | */ 50 | get("/{name}/movies", ctx -> { 51 | var userId = AppUtils.getUserId(ctx); 52 | var movies = movieService.byGenre(ctx.pathParam("name"), Params.parse(ctx, Params.MOVIE_SORT), userId); 53 | ctx.result(gson.toJson(movies)); 54 | }); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/neoflix/AuthUtils.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import at.favre.lib.crypto.bcrypt.BCrypt; 4 | import com.auth0.jwt.JWT; 5 | import com.auth0.jwt.JWTVerifier; 6 | import com.auth0.jwt.algorithms.Algorithm; 7 | import com.auth0.jwt.exceptions.JWTCreationException; 8 | import com.auth0.jwt.interfaces.DecodedJWT; 9 | 10 | import java.util.Calendar; 11 | import java.util.Date; 12 | import java.util.Map; 13 | 14 | public class AuthUtils { 15 | 16 | public static String encryptPassword(String password) { 17 | return BCrypt.withDefaults().hashToString(12, password.toCharArray()); 18 | } 19 | public static boolean verifyPassword(String password, String hashed) { 20 | BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), hashed); 21 | return result.verified; 22 | } 23 | 24 | public static String verify(String token, String secret) { 25 | // todo reuse 26 | Algorithm algorithm = Algorithm.HMAC256(secret); 27 | // todo reuse 28 | JWTVerifier verifier = JWT.require(algorithm) 29 | .withIssuer("auth0") 30 | .build(); //Reusable verifier instance 31 | DecodedJWT jwt = verifier.verify(token); 32 | return jwt.getSubject(); // sub == userId 33 | } 34 | 35 | public static String sign(String sub, Map data, String secret) { 36 | Algorithm algorithm = Algorithm.HMAC256(secret); 37 | try { 38 | Calendar cal = Calendar.getInstance(); 39 | cal.add(Calendar.DATE,1); 40 | String token = JWT.create() 41 | .withClaim(sub, data) 42 | .withIssuer("auth0") 43 | .withSubject(sub) 44 | .withIssuedAt(new Date()) 45 | .withExpiresAt(cal.getTime()) 46 | .sign(algorithm); 47 | return token; 48 | } catch (JWTCreationException exception){ 49 | //Invalid Signing configuration / Couldn't convert Claims. 50 | throw new RuntimeException(exception); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /diff/02-movie-lists.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/MovieService.java b/src/main/java/neoflix/services/MovieService.java 2 | index 10064b7..3c6b43b 100644 3 | --- a/src/main/java/neoflix/services/MovieService.java 4 | +++ b/src/main/java/neoflix/services/MovieService.java 5 | @@ -44,12 +44,36 @@ public class MovieService { 6 | */ 7 | // tag::all[] 8 | public List> all(Params params, String userId) { 9 | - // TODO: Open an Session 10 | - // TODO: Execute a query in a new Read Transaction 11 | - // TODO: Get a list of Movies from the Result 12 | - // TODO: Close the session 13 | - 14 | - return AppUtils.process(popular, params); 15 | + // Open a new session 16 | + try (var session = this.driver.session()) { 17 | + // tag::allcypher[] 18 | + // Execute a query in a new Read Transaction 19 | + var movies = session.executeRead(tx -> { 20 | + // Retrieve a list of movies with the 21 | + // favorite flag appened to the movie's properties 22 | + Params.Sort sort = params.sort(Params.Sort.title); 23 | + String query = String.format(""" 24 | + MATCH (m:Movie) 25 | + WHERE m.`%s` IS NOT NULL 26 | + RETURN m { 27 | + .* 28 | + } AS movie 29 | + ORDER BY m.`%s` %s 30 | + SKIP $skip 31 | + LIMIT $limit 32 | + """, sort, sort, params.order()); 33 | + var res= tx.run(query, Values.parameters( "skip", params.skip(), "limit", params.limit())); 34 | + // tag::allmovies[] 35 | + // Get a list of Movies from the Result 36 | + return res.list(row -> row.get("movie").asMap()); 37 | + // end::allmovies[] 38 | + }); 39 | + // end::allcypher[] 40 | + 41 | + // tag::return[] 42 | + return movies; 43 | + // end::return[] 44 | + } 45 | } 46 | // end::all[] 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/neoflix/routes/AuthRoutes.java: -------------------------------------------------------------------------------- 1 | package neoflix.routes; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import io.javalin.apibuilder.EndpointGroup; 6 | import neoflix.services.AuthService; 7 | import org.neo4j.driver.Driver; 8 | 9 | import static io.javalin.apibuilder.ApiBuilder.post; 10 | 11 | public class AuthRoutes implements EndpointGroup { 12 | private final Gson gson; 13 | private final AuthService authService; 14 | 15 | public AuthRoutes(Driver driver, Gson gson, String jwtSecret) { 16 | this.gson = gson; 17 | authService = new AuthService(driver, jwtSecret); 18 | } 19 | 20 | static class UserData { String email, name, password; }; 21 | 22 | @Override 23 | public void addEndpoints() { 24 | /* 25 | * @POST /auth/login 26 | * 27 | * Authenticates the user against the Neo4j database. 28 | * 29 | * The Authorization header contains a JWT token, which is used to authenticate the request. 30 | */ 31 | // tag::login[] 32 | post("/login", ctx -> { 33 | var userData = gson.fromJson(ctx.body(), UserData.class); 34 | var user = authService.authenticate(userData.email, userData.password); 35 | if (user != null) { 36 | ctx.attribute("user", user.get("userId")); 37 | } 38 | ctx.result(gson.toJson(user)); 39 | }); 40 | // end::login[] 41 | 42 | /* 43 | * @POST /auth/register 44 | * 45 | * This route should use the AuthService to create a new User node 46 | * in the database with an encrypted password before returning a User record which 47 | * includes a `token` property. This token is then used in the `JwtStrategy` from 48 | * `src/passport/jwt.strategy.js` to authenticate the request. 49 | */ 50 | // tag::register[] 51 | post("/register", ctx -> { 52 | var userData = gson.fromJson(ctx.body(), UserData.class); 53 | ctx.result(gson.toJson(authService.register(userData.email, userData.password, userData.name))); 54 | }); 55 | // end::register[] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_13_ListingRatingsTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.RatingService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | 9 | import java.util.Map; 10 | 11 | import static neoflix.Params.Sort.timestamp; 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | class _13_ListingRatingsTest { 15 | private static Driver driver; 16 | 17 | private static final String pulpFiction = "680"; 18 | 19 | @BeforeAll 20 | static void initDriver() { 21 | AppUtils.loadProperties(); 22 | driver = AppUtils.initDriver(); 23 | } 24 | 25 | @AfterAll 26 | static void closeDriver() { 27 | if (driver != null) driver.close(); 28 | } 29 | 30 | @Test 31 | void getListOfRatings() { 32 | RatingService ratingService = new RatingService(driver); 33 | 34 | var limit = 2; 35 | 36 | var output = ratingService.forMovie(pulpFiction, new Params(null, timestamp, Params.Order.DESC, limit, 0)); 37 | var paginated = ratingService.forMovie(pulpFiction, new Params(null, timestamp, Params.Order.DESC, limit, limit)); 38 | 39 | assertNotNull(output); 40 | assertEquals(limit, output.size()); 41 | 42 | assertNotEquals(output, paginated); 43 | } 44 | 45 | @Test 46 | void getOrderedPaginatedMovieRatings() { 47 | RatingService ratingService = new RatingService(driver); 48 | 49 | var limit = 1; 50 | 51 | var first = ratingService.forMovie(pulpFiction, new Params(null, timestamp, Params.Order.ASC, limit, 0)); 52 | var last = ratingService.forMovie(pulpFiction, new Params(null, timestamp, Params.Order.DESC, limit, 0)); 53 | 54 | assertNotEquals(first, last); 55 | 56 | System.out.println(""" 57 | 58 | Here is the answer to the quiz question on the lesson: 59 | What is the name of the first person to rate the movie Pulp Fiction? 60 | Copy and paste the following answer into the text box: 61 | """); 62 | System.out.println(((Map)first.get(0).get("user")).get("name")); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/neoflix/services/GenreService.java: -------------------------------------------------------------------------------- 1 | package neoflix.services; 2 | 3 | import neoflix.AppUtils; 4 | import org.neo4j.driver.Driver; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class GenreService { 10 | private final Driver driver; 11 | 12 | private final List> genres; 13 | 14 | public GenreService(Driver driver) { 15 | this.driver = driver; 16 | this.genres = AppUtils.loadFixtureList("genres"); 17 | } 18 | 19 | /** 20 | * This method should return a list of genres from the database with a 21 | * `name` property, `movies` which is the count of the incoming `IN_GENRE` 22 | * relationships and a `poster` property to be used as a background. 23 | * 24 | * [ 25 | * { 26 | * name: 'Action', 27 | * movies: 1545, 28 | * poster: 'https://image.tmdb.org/t/p/w440_and_h660_face/qJ2tW6WMUDux911r6m7haRef0WH.jpg' 29 | * }, ... 30 | * 31 | * ] 32 | * 33 | * @return List genres 34 | */ 35 | // tag::all[] 36 | public List> all() { 37 | // TODO: Open a new session 38 | // TODO: Get a list of Genres from the database 39 | // TODO: Close the session 40 | 41 | return genres; 42 | } 43 | // end::all[] 44 | 45 | /** 46 | * This method should find a Genre node by its name and return a set of properties 47 | * along with a `poster` image and `movies` count. 48 | * 49 | * If the genre is not found, a NotFoundError should be thrown. 50 | * 51 | * @param name The name of the genre 52 | * @return Genre The genre information 53 | */ 54 | // tag::find[] 55 | public Map find(String name) { 56 | // TODO: Open a new session 57 | // TODO: Get Genre information from the database 58 | // TODO: Throw a 404 Error if the genre is not found 59 | // TODO: Close the session 60 | 61 | return genres.stream() 62 | .filter(genre -> genre.get("name").equals(name)) 63 | .findFirst() 64 | .orElseThrow(() -> new RuntimeException("Genre "+name+" not found")); 65 | } 66 | // end::find[] 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_15_PersonProfileTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.PeopleService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | 9 | import static neoflix.Params.Sort.name; 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class _15_PersonProfileTest { 13 | private static Driver driver; 14 | 15 | private static final String coppola = "1776"; 16 | 17 | @BeforeAll 18 | static void initDriver() { 19 | AppUtils.loadProperties(); 20 | driver = AppUtils.initDriver(); 21 | } 22 | 23 | @AfterAll 24 | static void closeDriver() { 25 | if (driver != null) driver.close(); 26 | } 27 | 28 | @Test 29 | void findPersonById() { 30 | PeopleService peopleService = new PeopleService(driver); 31 | 32 | var output = peopleService.findById(coppola); 33 | assertNotNull(output); 34 | assertEquals(coppola, output.get("tmdbId")); 35 | assertEquals("Francis Ford Coppola", output.get("name")); 36 | assertEquals(16, Integer.parseInt(output.get("directedCount").toString())); 37 | assertEquals(2, Integer.parseInt(output.get("actedCount").toString())); 38 | } 39 | 40 | @Test 41 | void getSimilarPeopleByPersonId() { 42 | PeopleService peopleService = new PeopleService(driver); 43 | 44 | var limit = 2; 45 | 46 | var output = peopleService.getSimilarPeople(coppola, new Params(null, name, Params.Order.ASC, limit, 0)); 47 | assertNotNull(output); 48 | assertEquals(limit, output.size()); 49 | 50 | var secondOutput = peopleService.getSimilarPeople(coppola, new Params(null, name, Params.Order.ASC, limit, limit)); 51 | assertNotNull(secondOutput); 52 | assertEquals(limit, secondOutput.size()); 53 | assertNotEquals(output, secondOutput); 54 | 55 | System.out.println(""" 56 | 57 | Here is the answer to the quiz question on the lesson: 58 | According to our algorithm, who is the most similar person to Francis Ford Coppola? 59 | Copy and paste the following answer into the text box: 60 | """); 61 | System.out.println(output.get(0).get("name")); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /diff/10-genre-details.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/GenreService.java b/src/main/java/neoflix/services/GenreService.java 2 | index 5a6fc96..b9577c0 100644 3 | --- a/src/main/java/neoflix/services/GenreService.java 4 | +++ b/src/main/java/neoflix/services/GenreService.java 5 | @@ -2,6 +2,7 @@ package neoflix.services; 6 | 7 | import neoflix.AppUtils; 8 | import org.neo4j.driver.Driver; 9 | +import org.neo4j.driver.Values; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | @@ -78,15 +79,33 @@ public class GenreService { 14 | */ 15 | // tag::find[] 16 | public Map find(String name) { 17 | - // TODO: Open a new session 18 | - // TODO: Get Genre information from the database 19 | - // TODO: Throw a 404 Error if the genre is not found 20 | - // TODO: Close the session 21 | + // Open a new Session, close automatically at the end 22 | + try (var session = driver.session()) { 23 | + // Get a list of Genres from the database 24 | + var query = """ 25 | + MATCH (g:Genre {name: $name})<-[:IN_GENRE]-(m:Movie) 26 | + WHERE m.imdbRating IS NOT NULL 27 | + AND m.poster IS NOT NULL 28 | + AND g.name <> '(no genres listed)' 29 | + WITH g, m 30 | + ORDER BY m.imdbRating DESC 31 | + 32 | + WITH g, head(collect(m)) AS movie 33 | 34 | - return genres.stream() 35 | - .filter(genre -> genre.get("name").equals(name)) 36 | - .findFirst() 37 | - .orElseThrow(() -> new RuntimeException("Genre "+name+" not found")); 38 | + RETURN g { 39 | + link: '/genres/'+ g.name, 40 | + .name, 41 | + movies: size((g)<-[:IN_GENRE]-()), 42 | + poster: movie.poster 43 | + } AS genre 44 | + """; 45 | + var genre = session.executeRead( 46 | + tx -> tx.run(query, Values.parameters("name", name)) 47 | + // Throw a NoSuchRecordException if the genre is not found 48 | + .single().get("genre").asMap()); 49 | + // Return results 50 | + return genre; 51 | + } 52 | } 53 | // end::find[] 54 | } 55 | -------------------------------------------------------------------------------- /diff/06-rating-movies.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/RatingService.java b/src/main/java/neoflix/services/RatingService.java 2 | index 45e06e4..1b04ee1 100644 3 | --- a/src/main/java/neoflix/services/RatingService.java 4 | +++ b/src/main/java/neoflix/services/RatingService.java 5 | @@ -2,7 +2,11 @@ package neoflix.services; 6 | 7 | import neoflix.AppUtils; 8 | import neoflix.Params; 9 | +import neoflix.ValidationException; 10 | + 11 | import org.neo4j.driver.Driver; 12 | +import org.neo4j.driver.Values; 13 | +import org.neo4j.driver.exceptions.NoSuchRecordException; 14 | 15 | import java.util.HashMap; 16 | import java.util.List; 17 | @@ -60,13 +64,37 @@ public class RatingService { 18 | */ 19 | // tag::add[] 20 | public Map add(String userId, String movieId, int rating) { 21 | - // TODO: Convert the native integer into a Neo4j Integer 22 | - // TODO: Save the rating in the database 23 | - // TODO: Return movie details and a rating 24 | + // tag::write[] 25 | + // Save the rating in the database 26 | + 27 | + // Open a new session 28 | + try (var session = this.driver.session()) { 29 | + 30 | + // Run the cypher query 31 | + var movie = session.executeWrite(tx -> { 32 | + String query = """ 33 | + MATCH (u:User {userId: $userId}) 34 | + MATCH (m:Movie {tmdbId: $movieId}) 35 | + 36 | + MERGE (u)-[r:RATED]->(m) 37 | + SET r.rating = $rating, r.timestamp = timestamp() 38 | + 39 | + RETURN m { .*, rating: r.rating } AS movie 40 | + """; 41 | + var res = tx.run(query, Values.parameters("userId", userId, "movieId", movieId, "rating", rating)); 42 | + return res.single().get("movie").asMap(); 43 | + }); 44 | + // end::write[] 45 | 46 | - var copy = new HashMap<>(pulpfiction); 47 | - copy.put("rating",rating); 48 | - return copy; 49 | + // tag::addreturn[] 50 | + // Return movie details with rating 51 | + return movie; 52 | + // end::addreturn[] 53 | + // tag::throw[] 54 | + } catch(NoSuchRecordException e) { 55 | + throw new ValidationException("Movie or user not found to add rating", Map.of("movie", movieId, "user", userId)); 56 | + } 57 | + // end::throw[] 58 | } 59 | // end::add[] 60 | } 61 | \ No newline at end of file 62 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_12_MovieDetailsTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.MovieService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Values; 9 | 10 | import static neoflix.Params.Sort.title; 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class _12_MovieDetailsTest { 14 | private static Driver driver; 15 | 16 | private static final String userId = "fe770c6b-4034-4e07-8e40-2f39e7a6722c"; 17 | private static final String email = "graphacademy.movielists@neo4j.com"; 18 | private static final String lockStock = "100"; 19 | 20 | @BeforeAll 21 | static void initDriver() { 22 | AppUtils.loadProperties(); 23 | driver = AppUtils.initDriver(); 24 | if (driver != null) 25 | driver.session().executeWrite(tx -> tx.run(""" 26 | MERGE (u:User {userId: $userId}) SET u.email = $email 27 | """, Values.parameters("userId", userId, "email", email))); 28 | } 29 | 30 | @AfterAll 31 | static void closeDriver() { 32 | if (driver != null) driver.close(); 33 | } 34 | 35 | @Test 36 | void getMovieById() { 37 | MovieService movieService = new MovieService(driver); 38 | 39 | var output = movieService.findById(lockStock, userId); 40 | assertNotNull(output); 41 | assertEquals(lockStock, output.get("tmdbId")); 42 | assertEquals("Lock, Stock & Two Smoking Barrels", output.get("title")); 43 | } 44 | 45 | @Test 46 | void getSimilarMoviesByScore() { 47 | MovieService movieService = new MovieService(driver); 48 | 49 | var limit = 1; 50 | 51 | var output = movieService.getSimilarMovies(lockStock, new Params(null, title, Params.Order.ASC, limit, 0), userId); 52 | var paginated = movieService.getSimilarMovies(lockStock, new Params(null, title, Params.Order.ASC, limit, 1), userId); 53 | 54 | assertNotNull(output); 55 | assertEquals(limit, output.size()); 56 | 57 | assertNotEquals(output, paginated); 58 | 59 | System.out.println(""" 60 | 61 | Here is the answer to the quiz question on the lesson: 62 | What is the title of the most similar movie to Lock, Stock & Two Smoking Barrels? 63 | Copy and paste the following answer into the text box: 64 | """); 65 | System.out.println(output.get(0).get("title")); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_03_RegisterUserTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.AuthService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Values; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class _03_RegisterUserTest { 13 | 14 | private static Driver driver; 15 | private static String jwtSecret; 16 | 17 | private static final String email = "graphacademy.register@neo4j.com"; 18 | private static final String password = "letmein"; 19 | private static final String name = "Graph Academy"; 20 | 21 | @BeforeAll 22 | static void initDriverAuth() { 23 | AppUtils.loadProperties(); 24 | driver = AppUtils.initDriver(); 25 | jwtSecret = AppUtils.getJwtSecret(); 26 | 27 | if (driver != null) driver.session().executeWrite(tx -> tx.run("MATCH (u:User {email: $email}) DETACH DELETE u", Values.parameters("email", email))); 28 | } 29 | 30 | @AfterAll 31 | static void closeDriver() { 32 | if (driver != null) driver.close(); 33 | } 34 | 35 | @Test 36 | void registerUser() { 37 | AuthService authService = new AuthService(driver, jwtSecret); 38 | var output = authService.register(email, password, name); 39 | assertNotNull(output); 40 | assertEquals(4, output.size(), "4 properties returned"); 41 | 42 | assertEquals(email, output.get("email"), "email property"); 43 | assertEquals(name, output.get("name"), "name property"); 44 | assertNotNull(output.get("token"), "token property generated"); 45 | assertNotNull(output.get("userId"), "userId property generated"); 46 | assertNull(output.get("password"), "no password returned"); 47 | 48 | // Expect user exists in database 49 | if (driver != null) try (var session = driver.session()) { 50 | session.executeRead(tx -> { 51 | var user = tx.run( 52 | "MATCH (u:User {email: $email}) RETURN u", 53 | Values.parameters("email", email)) 54 | .single().get("u").asMap(); 55 | 56 | assertEquals(email, user.get("email"), "email property"); 57 | assertEquals(name, user.get("name"), "name property"); 58 | assertNotNull(user.get("userId"), "userId property generated"); 59 | assertNotEquals(password, user.get("password"), "password was hashed"); 60 | return null; 61 | }); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/example/AsyncApi.java: -------------------------------------------------------------------------------- 1 | package example; 2 | // tag::import[] 3 | // Import all relevant classes from neo4j-java-driver dependency 4 | import neoflix.AppUtils; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | import org.neo4j.driver.*; 8 | import org.neo4j.driver.reactive.RxSession; 9 | // end::import[] 10 | 11 | import java.util.*; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.logging.Level; 14 | 15 | public class AsyncApi { 16 | 17 | static { 18 | // Load config from .env 19 | AppUtils.loadProperties(); 20 | } 21 | 22 | // Load Driver 23 | static Driver driver = GraphDatabase.driver(System.getProperty("NEO4J_URI"), 24 | AuthTokens.basic(System.getProperty("NEO4J_USERNAME"), System.getProperty("NEO4J_PASSWORD"))); 25 | 26 | static void syncExample() { 27 | // tag::sync[] 28 | try (var session = driver.session()) { 29 | 30 | var res = session.readTransaction(tx -> tx.run( 31 | "MATCH (p:Person) RETURN p.name AS name LIMIT 10").list()); 32 | res.stream() 33 | .map(row -> row.get("name")) 34 | .forEach(System.out::println); 35 | } catch (Exception e) { 36 | // There was a problem with the 37 | // database connection or the query 38 | e.printStackTrace(); 39 | } 40 | // end::sync[] 41 | } 42 | 43 | static void asyncExample() { 44 | // tag::async[] 45 | var session = driver.asyncSession(); 46 | session.readTransactionAsync(tx -> tx.runAsync( 47 | "MATCH (p:Person) RETURN p.name AS name LIMIT 10") 48 | 49 | .thenApplyAsync(res -> res.listAsync(row -> row.get("name"))) 50 | .thenAcceptAsync(System.out::println) 51 | .exceptionallyAsync(e -> { 52 | e.printStackTrace(); 53 | return null; 54 | }) 55 | ); 56 | // end::async[] 57 | } 58 | 59 | static void reactiveExample() { 60 | // tag::reactive[] 61 | Flux.usingWhen(Mono.fromSupplier(driver::rxSession), 62 | session -> session.readTransaction(tx -> { 63 | var rxResult = tx.run( 64 | "MATCH (p:Person) RETURN p.name AS name LIMIT 10"); 65 | return Flux 66 | .from(rxResult.records()) 67 | .map(r -> r.get("name").asString()) 68 | .doOnNext(System.out::println) 69 | .then(Mono.from(rxResult.consume())); 70 | } 71 | ), RxSession::close); 72 | // end::reactive[] 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/resources/public/js/movies.latest.006f38b5.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/views/MovieList.vue?8295","webpack:///./src/views/MovieList.vue","webpack:///./src/views/MovieList.vue?c38a"],"names":["render","_ctx","_cache","$props","$setup","$data","$options","_component_movie_grid","title","to","path","sort","order","showLoadMore","components","MovieGrid","setup","route","substr","__exports__"],"mappings":"kJAEM,SAAUA,EAAOC,EAAUC,EAAYC,EAAYC,EAAYC,EAAWC,GAC9E,IAAMC,EAAwB,eAAkB,cAEhD,OAAQ,iBAAc,eAAaA,EAAuB,CACxDC,MAAOP,EAAKO,MACZC,GAAIR,EAAKS,KACTC,KAAMV,EAAKU,KACXC,MAAOX,EAAKW,MACZC,cAAc,GACb,KAAM,EAAG,CAAC,QAAS,KAAM,OAAQ,U,oDCJvB,iBAAgB,CAC7BC,WAAY,CACVC,YAAA,MAEFC,MAJ6B,WAK3B,IAAMC,EAAQ,iBACRP,EAAOO,EAAMP,KAAKQ,OAAO,GAE3BV,EAAQ,SACRG,EAAqB,OAEzB,OAAQD,GACN,IAAK,UACHF,EAAQ,iBACRG,EAAO,OACP,MAEF,IAAK,SACHH,EAAQ,kBACRG,EAAO,OACP,MAGJ,MAAO,CACLH,QACAE,OACAC,OACAC,MAAO,W,qBC7Bb,MAAMO,EAA2B,IAAgB,EAAQ,CAAC,CAAC,SAASnB,KAErD","file":"js/movies.latest.006f38b5.js","sourcesContent":["import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from \"vue\"\n\nexport function render(_ctx: any,_cache: any,$props: any,$setup: any,$data: any,$options: any) {\n const _component_movie_grid = _resolveComponent(\"movie-grid\")!\n\n return (_openBlock(), _createBlock(_component_movie_grid, {\n title: _ctx.title,\n to: _ctx.path,\n sort: _ctx.sort,\n order: _ctx.order,\n showLoadMore: true\n }, null, 8, [\"title\", \"to\", \"sort\", \"order\"]))\n}","\nimport { defineComponent } from 'vue'\nimport { useRoute } from 'vue-router'\nimport { MovieOrderBy, ORDER_BY_RATING, ORDER_BY_RELEASE } from '@/modules/movies'\nimport { ORDER_DESC } from '@/modules/api'\nimport MovieGrid from '@/components/ui/home/MovieGrid.vue'\n\nexport default defineComponent({\n components: {\n MovieGrid,\n },\n setup() {\n const route = useRoute()\n const path = route.path.substr(1)\n\n let title = 'Movies'\n let sort: MovieOrderBy = ORDER_BY_RELEASE\n\n switch (path) {\n case 'popular':\n title = 'Popular Movies'\n sort = ORDER_BY_RATING\n break\n\n case 'latest':\n title = 'Latest Releases'\n sort = ORDER_BY_RELEASE\n break\n }\n\n return {\n title,\n path,\n sort,\n order: ORDER_DESC,\n }\n },\n})\n","import { render } from \"./MovieList.vue?vue&type=template&id=20e009cb&ts=true\"\nimport script from \"./MovieList.vue?vue&type=script&lang=ts\"\nexport * from \"./MovieList.vue?vue&type=script&lang=ts\"\n\nimport exportComponent from \"/Users/adam/graphacademy/neoflix-ui/node_modules/vue-loader-v16/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render]])\n\nexport default __exports__"],"sourceRoot":""} -------------------------------------------------------------------------------- /src/test/java/neoflix/_14_PersonListTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.PeopleService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | 9 | import static neoflix.Params.Sort.name; 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class _14_PersonListTest { 13 | private static Driver driver; 14 | 15 | @BeforeAll 16 | static void initDriver() { 17 | AppUtils.loadProperties(); 18 | driver = AppUtils.initDriver(); 19 | } 20 | 21 | @AfterAll 22 | static void closeDriver() { 23 | if (driver != null) driver.close(); 24 | } 25 | 26 | @Test 27 | void getPaginatedPersonList() { 28 | PeopleService peopleService = new PeopleService(driver); 29 | 30 | var limit = 3; 31 | 32 | var output = peopleService.all(new Params(null, name, Params.Order.ASC, limit, 0)); 33 | assertNotNull(output); 34 | assertEquals(limit, output.size()); 35 | assertEquals("'Snub' Pollard", output.get(0).get("name")); 36 | 37 | var paginated = peopleService.all(new Params(null, name, Params.Order.ASC, limit, limit)); 38 | assertNotNull(paginated); 39 | assertEquals(limit, paginated.size()); 40 | assertNotEquals(output, paginated); 41 | assertEquals("50 Cent", paginated.get(0).get("name")); 42 | } 43 | 44 | @Test 45 | void getOrderedPaginatedPersonList() { 46 | PeopleService peopleService = new PeopleService(driver); 47 | 48 | var q = "A"; 49 | 50 | var first = peopleService.all(new Params(q, name, Params.Order.ASC, 1, 0)); 51 | var last = peopleService.all(new Params(q, name, Params.Order.DESC, 1, 0)); 52 | assertNotNull(first); 53 | assertEquals(1, first.size()); 54 | assertEquals("'Spring' Mark Adley", first.get(0).get("name")); 55 | assertNotEquals(first, last); 56 | assertEquals("Álex Angulo", last.get(0).get("name")); 57 | } 58 | 59 | @Test 60 | void getOrderedPaginatedQueryForPersons() { 61 | PeopleService peopleService = new PeopleService(driver); 62 | 63 | var first = peopleService.all(new Params(null, name, Params.Order.ASC, 1, 0)); 64 | assertNotNull(first); 65 | assertEquals(1, first.size()); 66 | 67 | System.out.println(""" 68 | 69 | Here is the answer to the quiz question on the lesson: 70 | What is the name of the first person in the database in alphabetical order? 71 | Copy and paste the following answer into the text box: 72 | """); 73 | System.out.println(first.get(0).get("name").toString().trim()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/neoflix/routes/PeopleRoutes.java: -------------------------------------------------------------------------------- 1 | package neoflix.routes; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import io.javalin.apibuilder.EndpointGroup; 6 | import neoflix.Params; 7 | import neoflix.AppUtils; 8 | import neoflix.services.MovieService; 9 | import neoflix.services.PeopleService; 10 | import org.neo4j.driver.Driver; 11 | 12 | import static io.javalin.apibuilder.ApiBuilder.get; 13 | 14 | public class PeopleRoutes implements EndpointGroup { 15 | private final Gson gson; 16 | private final PeopleService peopleService; 17 | private final MovieService movieService; 18 | 19 | public PeopleRoutes(Driver driver, Gson gson) { 20 | this.gson = gson; 21 | peopleService = new PeopleService(driver); 22 | movieService = new MovieService(driver); 23 | } 24 | 25 | @Override 26 | public void addEndpoints() { 27 | /* 28 | * @GET /people/ 29 | * 30 | * This route should return a paginated list of People from the database 31 | */ 32 | get("", ctx -> ctx.result(gson.toJson(peopleService.all(Params.parse(ctx, Params.PEOPLE_SORT))))); 33 | 34 | /* 35 | * @GET /people/{id} 36 | * 37 | * This route should the properties of a Person based on their tmdbId 38 | */ 39 | get("/{id}", ctx -> ctx.result(gson.toJson(peopleService.findById(ctx.pathParam("id"))))); 40 | 41 | /* 42 | * @GET /people/{id}/similar 43 | * 44 | * This route should return a paginated list of similar people to the person 45 | * with the {id} supplied in the route params. 46 | */ 47 | get("/{id}/similar", ctx -> ctx.result(gson.toJson(peopleService.getSimilarPeople(ctx.pathParam("id"), Params.parse(ctx, Params.PEOPLE_SORT))))); 48 | 49 | /* 50 | * @GET /people/{id}/acted 51 | * 52 | * This route should return a paginated list of movies that the person 53 | * with the {id} has acted in. 54 | */ 55 | get("/{id}/acted", ctx -> { 56 | var userId = AppUtils.getUserId(ctx); 57 | var movies = movieService.getForActor(ctx.pathParam("id"), Params.parse(ctx, Params.MOVIE_SORT), userId); 58 | ctx.result(gson.toJson(movies)); 59 | }); 60 | 61 | /* 62 | * @GET /people/{id}/directed 63 | * 64 | * This route should return a paginated list of movies that the person 65 | * with the {id} has directed. 66 | */ 67 | get("/{id}/directed", ctx -> { 68 | var userId = AppUtils.getUserId(ctx); 69 | var movies = movieService.getForDirector(ctx.pathParam("id"), Params.parse(ctx, Params.MOVIE_SORT), userId); 70 | ctx.result(gson.toJson(movies)); 71 | }); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/neoflix/services/RatingService.java: -------------------------------------------------------------------------------- 1 | package neoflix.services; 2 | 3 | import neoflix.AppUtils; 4 | import neoflix.Params; 5 | import org.neo4j.driver.Driver; 6 | 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class RatingService { 12 | private final Driver driver; 13 | private final List> ratings; 14 | private final Map pulpfiction; 15 | 16 | /** 17 | * The constructor expects an instance of the Neo4j Driver, which will be 18 | * used to interact with Neo4j. 19 | */ 20 | public RatingService(Driver driver) { 21 | this.driver = driver; 22 | this.ratings = AppUtils.loadFixtureList("ratings"); 23 | this.pulpfiction = AppUtils.loadFixtureSingle("pulpfiction"); 24 | } 25 | 26 | /** 27 | * Return a paginated list of reviews for a Movie. 28 | * 29 | * Results should be ordered by the `sort` parameter, and in the direction specified 30 | * in the `order` parameter. 31 | * Results should be limited to the number passed as `limit`. 32 | * The `skip` variable should be used to skip a certain number of rows. 33 | * 34 | * @param {string} id The tmdbId for the movie 35 | * @param {string} sort The field to order the results by 36 | * @param {string} order The direction of the order (ASC/DESC) 37 | * @param {number} limit The total number of records to return 38 | * @param {number} skip The number of records to skip 39 | * @returns {Promise>} 40 | */ 41 | // tag::forMovie[] 42 | public List> forMovie(String id, Params params) { 43 | // TODO: Get ratings for a Movie 44 | 45 | return AppUtils.process(ratings,params); 46 | } 47 | // end::forMovie[] 48 | 49 | 50 | /** 51 | * Add a relationship between a User and Movie with a `rating` property. 52 | * The `rating` parameter should be converted to a Neo4j Integer. 53 | * 54 | * If the User or Movie cannot be found, a NotFoundError should be thrown 55 | * 56 | * @param {string} userId the userId for the user 57 | * @param {string} movieId The tmdbId for the Movie 58 | * @param {number} rating An integer representing the rating from 1-5 59 | * @returns {Promise>} A movie object with a rating property appended 60 | */ 61 | // tag::add[] 62 | public Map add(String userId, String movieId, int rating) { 63 | // TODO: Convert the native integer into a Neo4j Integer 64 | // TODO: Save the rating in the database 65 | // TODO: Return movie details and a rating 66 | 67 | var copy = new HashMap<>(pulpfiction); 68 | copy.put("rating",rating); 69 | return copy; 70 | } 71 | // end::add[] 72 | } -------------------------------------------------------------------------------- /src/test/java/neoflix/_08_FavoriteFlagTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.FavoriteService; 4 | import neoflix.services.MovieService; 5 | import org.junit.jupiter.api.*; 6 | import org.neo4j.driver.Driver; 7 | import org.neo4j.driver.Values; 8 | 9 | import static neoflix.Params.Order.DESC; 10 | import static neoflix.Params.Sort.imdbRating; 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class _08_FavoriteFlagTest { 14 | private static Driver driver; 15 | 16 | private static String userId = "fe770c6b-4034-4e07-8e40-2f39e7a6722c"; 17 | private static final String email = "graphacademy.flag@neo4j.com"; // users get an error since this email already exists, might want to remove or change it before this module 18 | 19 | @BeforeAll 20 | static void initDriver() { 21 | AppUtils.loadProperties(); 22 | driver = AppUtils.initDriver(); 23 | } 24 | 25 | @AfterAll 26 | static void closeDriver() { 27 | if (driver != null) driver.close(); 28 | } 29 | 30 | @BeforeEach 31 | void setUp() { 32 | if (driver != null) try (var session = driver.session()) { 33 | session.executeWrite(tx -> 34 | tx.run(""" 35 | MERGE (u:User {userId: $userId}) 36 | SET u.email = $email 37 | 38 | FOREACH (r IN [ (u)-[r:HAS_FAVORITE]->() |r ] | DELETE r) 39 | """, 40 | Values.parameters("userId", userId, "email", email))); 41 | } 42 | } 43 | 44 | @Test 45 | void favoriteMovieReturnsFlaggedInMovieList() { 46 | MovieService movieService = new MovieService(driver); 47 | FavoriteService favoriteService = new FavoriteService(driver); 48 | 49 | // Get the most popular movie 50 | var topMovie = movieService.all(new Params(null, imdbRating, DESC, 1, 0), userId); 51 | 52 | // Add top movie to user favorites 53 | var topMovieId = topMovie.get(0).get("tmdbId").toString(); 54 | var add = favoriteService.add(userId, topMovieId); 55 | assertEquals(topMovieId, add.get("tmdbId")); 56 | assertTrue((Boolean)add.get("favorite"), "top movie is favorite"); 57 | 58 | var addCheck = favoriteService.all(userId, new Params(null, imdbRating, Params.Order.ASC, 999, 0)); 59 | 60 | assertEquals(1, addCheck.size()); 61 | var found = addCheck.stream().allMatch(movie -> movie.get("tmdbId").equals(topMovieId)); 62 | assertTrue(found); 63 | 64 | var topTwo = movieService.all(new Params(null, imdbRating, DESC, 2, 0), userId); 65 | assertEquals(topMovieId, topTwo.get(0).get("tmdbId")); 66 | 67 | Assumptions.assumeTrue(topTwo.get(0).get("favorite") != null); 68 | assertEquals(true, topTwo.get(0).get("favorite")); 69 | assertEquals(false, topTwo.get(1).get("favorite")); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /diff/08-favorite-flag.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/MovieService.java b/src/main/java/neoflix/services/MovieService.java 2 | index 3c6b43b..fdf7591 100644 3 | --- a/src/main/java/neoflix/services/MovieService.java 4 | +++ b/src/main/java/neoflix/services/MovieService.java 5 | @@ -5,6 +5,7 @@ import neoflix.NeoflixApp; 6 | import neoflix.Params; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Transaction; 9 | +import org.neo4j.driver.TransactionContext; 10 | import org.neo4j.driver.Values; 11 | 12 | import java.util.HashMap; 13 | @@ -49,6 +50,9 @@ public class MovieService { 14 | // tag::allcypher[] 15 | // Execute a query in a new Read Transaction 16 | var movies = session.executeRead(tx -> { 17 | + // Get an array of IDs for the User's favorite movies 18 | + var favorites = getUserFavorites(tx, userId); 19 | + 20 | // Retrieve a list of movies with the 21 | // favorite flag appened to the movie's properties 22 | Params.Sort sort = params.sort(Params.Sort.title); 23 | @@ -56,13 +60,14 @@ public class MovieService { 24 | MATCH (m:Movie) 25 | WHERE m.`%s` IS NOT NULL 26 | RETURN m { 27 | - .* 28 | + .*, 29 | + favorite: m.tmdbId IN $favorites 30 | } AS movie 31 | ORDER BY m.`%s` %s 32 | SKIP $skip 33 | LIMIT $limit 34 | """, sort, sort, params.order()); 35 | - var res= tx.run(query, Values.parameters( "skip", params.skip(), "limit", params.limit())); 36 | + var res= tx.run(query, Values.parameters( "skip", params.skip(), "limit", params.limit(), "favorites",favorites)); 37 | // tag::allmovies[] 38 | // Get a list of Movies from the Result 39 | return res.list(row -> row.get("movie").asMap()); 40 | @@ -220,8 +225,15 @@ public class MovieService { 41 | * @return List movieIds of favorite movies 42 | */ 43 | // tag::getUserFavorites[] 44 | - private List getUserFavorites(Transaction tx, String userId) { 45 | - return List.of(); 46 | + private List getUserFavorites(TransactionContext tx, String userId) { 47 | + // If userId is not defined, return an empty list 48 | + if (userId == null) return List.of(); 49 | + var favoriteResult = tx.run(""" 50 | + MATCH (u:User {userId: $userId})-[:HAS_FAVORITE]->(m) 51 | + RETURN m.tmdbId AS id 52 | + """, Values.parameters("userId",userId)); 53 | + // Extract the `id` value returned by the cypher query 54 | + return favoriteResult.list(row -> row.get("id").asString()); 55 | } 56 | // end::getUserFavorites[] 57 | 58 | -------------------------------------------------------------------------------- /diff/05-authentication.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/AuthService.java b/src/main/java/neoflix/services/AuthService.java 2 | index 8c83d07..b71042d 100644 3 | --- a/src/main/java/neoflix/services/AuthService.java 4 | +++ b/src/main/java/neoflix/services/AuthService.java 5 | @@ -6,6 +6,7 @@ import neoflix.ValidationException; 6 | import org.neo4j.driver.Driver; 7 | import org.neo4j.driver.Values; 8 | import org.neo4j.driver.exceptions.ClientException; 9 | +import org.neo4j.driver.exceptions.NoSuchRecordException; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | @@ -108,20 +109,35 @@ public class AuthService { 14 | */ 15 | // tag::authenticate[] 16 | public Map authenticate(String email, String plainPassword) { 17 | - // TODO: Authenticate the user from the database 18 | - var foundUser = users.stream().filter(u -> u.get("email").equals(email)).findAny(); 19 | - if (foundUser.isEmpty()) 20 | + // Open a new Session 21 | + try (var session = this.driver.session()) { 22 | + // tag::query[] 23 | + // Find the User node within a Read Transaction 24 | + var user = session.executeRead(tx -> { 25 | + String statement = "MATCH (u:User {email: $email}) RETURN u"; 26 | + var res = tx.run(statement, Values.parameters("email", email)); 27 | + return res.single().get("u").asMap(); 28 | + 29 | + }); 30 | + // end::query[] 31 | + 32 | + // tag::password[] 33 | + // Check password 34 | + if (!AuthUtils.verifyPassword(plainPassword, (String)user.get("password"))) { 35 | + throw new ValidationException("Incorrect password", Map.of("password","Incorrect password")); 36 | + } 37 | + // end::password[] 38 | + 39 | + // tag::auth-return[] 40 | + String sub = (String)user.get("userId"); 41 | + String token = AuthUtils.sign(sub, userToClaims(user), jwtSecret); 42 | + return userWithToken(user, token); 43 | + // end::auth-return[] 44 | + // tag::auth-catch[] 45 | + } catch(NoSuchRecordException e) { 46 | throw new ValidationException("Incorrect email", Map.of("email","Incorrect email")); 47 | - var user = foundUser.get(); 48 | - if (!plainPassword.equals(user.get("password")) && 49 | - !AuthUtils.verifyPassword(plainPassword,(String)user.get("password"))) { // 50 | - throw new ValidationException("Incorrect password", Map.of("password","Incorrect password")); 51 | } 52 | - // tag::return[] 53 | - String sub = (String) user.get("userId"); 54 | - String token = AuthUtils.sign(sub, userToClaims(user), jwtSecret); 55 | - return userWithToken(user, token); 56 | - // end::return[] 57 | + // end::auth-catch[] 58 | } 59 | // end::authenticate[] 60 | 61 | -------------------------------------------------------------------------------- /diff/15-person-profile.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/PeopleService.java b/src/main/java/neoflix/services/PeopleService.java 2 | index a3970eb..f3ce092 100644 3 | --- a/src/main/java/neoflix/services/PeopleService.java 4 | +++ b/src/main/java/neoflix/services/PeopleService.java 5 | @@ -73,9 +73,24 @@ public class PeopleService { 6 | */ 7 | // tag::findById[] 8 | public Map findById(String id) { 9 | - // TODO: Find a user by their ID 10 | + // Open a new database session 11 | + try (var session = driver.session()) { 12 | 13 | - return people.stream().filter(p -> id.equals(p.get("tmdbId"))).findAny().get(); 14 | + // Get a person from the database 15 | + var person = session.executeRead(tx -> { 16 | + String query = """ 17 | + MATCH (p:Person {tmdbId: $id}) 18 | + RETURN p { 19 | + .*, 20 | + actedCount: size((p)-[:ACTED_IN]->()), 21 | + directedCount: size((p)-[:DIRECTED]->()) 22 | + } AS person 23 | + """; 24 | + var res = tx.run(query, Values.parameters("id", id)); 25 | + return res.single().get("person").asMap(); 26 | + }); 27 | + return person; 28 | + } 29 | } 30 | // end::findById[] 31 | 32 | @@ -89,9 +104,26 @@ public class PeopleService { 33 | */ 34 | // tag::getSimilarPeople[] 35 | public List> getSimilarPeople(String id, Params params) { 36 | - // TODO: Get a list of similar people to the person by their id 37 | + // Open a new database session 38 | + try (var session = driver.session()) { 39 | + 40 | + // Get a list of similar people to the person by their id 41 | + var res = session.executeRead(tx -> tx.run(""" 42 | + MATCH (:Person {tmdbId: $id})-[:ACTED_IN|DIRECTED]->(m)<-[r:ACTED_IN|DIRECTED]-(p) 43 | + RETURN p { 44 | + .*, 45 | + actedCount: size((p)-[:ACTED_IN]->()), 46 | + directedCount: size((p)-[:DIRECTED]->()), 47 | + inCommon: collect(m {.tmdbId, .title, type: type(r)}) 48 | + } AS person 49 | + ORDER BY size(person.inCommon) DESC 50 | + SKIP $skip 51 | + LIMIT $limit 52 | + """,Values.parameters("id",id, "skip", params.skip(), "limit", params.limit())) 53 | + .list(row -> row.get("person").asMap())); 54 | 55 | - return AppUtils.process(people, params); 56 | + return res; 57 | + } 58 | } 59 | // end::getSimilarPeople[] 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/public/js/favorites.ff43e072.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["favorites"],{4559:function(t,e,n){"use strict";var c=n("7a23"),o={class:"col-6 col-sm-4 col-lg-3 col-xl-2"};function i(t,e){return Object(c["u"])(),Object(c["f"])("div",o,[Object(c["B"])(t.$slots,"default")])}var r=n("6b0d"),u=n.n(r);const a={},b=u()(a,[["render",i]]);e["a"]=b},"48d5":function(t,e,n){"use strict";n.r(e);var c=n("7a23"),o=Object(c["g"])("p",null,"You must be logged in to save your favorite movies.",-1),i=Object(c["i"])("Sign in"),r=Object(c["i"])(" or "),u=Object(c["i"])("Register"),a=Object(c["i"])(" to continue. "),b=Object(c["g"])("p",null,"Your favorite movies will be listed here.",-1),l=Object(c["g"])("p",null,"Click the icon in the top right hand corner of a Movie to save it to your favorites.",-1),j=Object(c["i"])("View Popular Movies"),d=Object(c["i"])(" or "),O=Object(c["i"])("check out the latest releases"),s=Object(c["i"])(". ");function f(t,e,n,f,v,g){var m=Object(c["C"])("router-link"),p=Object(c["C"])("column"),k=Object(c["C"])("grid"),K=Object(c["C"])("MovieGridItem"),_=Object(c["C"])("Section");return Object(c["u"])(),Object(c["d"])(_,{title:"My Favorite Movies",to:"/genres"},{default:Object(c["K"])((function(){return[401===t.code?(Object(c["u"])(),Object(c["d"])(k,{key:0},{default:Object(c["K"])((function(){return[Object(c["j"])(p,null,{default:Object(c["K"])((function(){return[o,Object(c["g"])("p",null,[Object(c["j"])(m,{to:{name:"Login"}},{default:Object(c["K"])((function(){return[i]})),_:1}),r,Object(c["j"])(m,{to:{name:"Register"}},{default:Object(c["K"])((function(){return[u]})),_:1}),a])]})),_:1})]})),_:1})):t.data&&t.data.length?(Object(c["u"])(),Object(c["d"])(k,{key:1},{default:Object(c["K"])((function(){return[(Object(c["u"])(!0),Object(c["f"])(c["a"],null,Object(c["A"])(t.data,(function(t){return Object(c["u"])(),Object(c["d"])(K,{key:t.tmdbId,to:{name:"MovieView",params:{tmdbId:t.tmdbId}},tmdbId:t.tmdbId,title:t.title,imdbRating:t.imdbRating,rating:t.rating,poster:t.poster,list:t.genres,favorite:t.favorite},null,8,["to","tmdbId","title","imdbRating","rating","poster","list","favorite"])})),128))]})),_:1})):(Object(c["u"])(),Object(c["d"])(k,{key:2},{default:Object(c["K"])((function(){return[Object(c["j"])(p,null,{default:Object(c["K"])((function(){return[b,l,Object(c["g"])("p",null,[Object(c["j"])(m,{to:{name:"PopularMovies"}},{default:Object(c["K"])((function(){return[j]})),_:1}),d,Object(c["j"])(m,{to:{name:"LatestMovies"}},{default:Object(c["K"])((function(){return[O]})),_:1}),s])]})),_:1})]})),_:1}))]})),_:1})}var v=n("5e73"),g=n("8317"),m=n("4559"),p=n("c2c0"),k=n("efbf"),K=Object(c["k"])({components:{Section:g["a"],Column:m["a"],Grid:p["a"],MovieGridItem:k["a"]},setup:function(){var t=Object(v["c"])("/account/favorites"),e=t.loading,n=t.data,c=t.error,o=t.code;return{loading:e,data:n,error:c,code:o}}}),_=n("6b0d"),h=n.n(_);const w=h()(K,[["render",f]]);e["default"]=w}}]); 2 | //# sourceMappingURL=favorites.ff43e072.js.map -------------------------------------------------------------------------------- /src/main/java/neoflix/services/PeopleService.java: -------------------------------------------------------------------------------- 1 | package neoflix.services; 2 | 3 | import neoflix.AppUtils; 4 | import neoflix.AuthUtils; 5 | import neoflix.Params; 6 | import org.neo4j.driver.Driver; 7 | import org.neo4j.driver.Values; 8 | 9 | import java.util.Comparator; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class PeopleService { 14 | private final Driver driver; 15 | private final List> people; 16 | 17 | /** 18 | * The constructor expects an instance of the Neo4j Driver, which will be 19 | * used to interact with Neo4j. 20 | * 21 | * @param driver 22 | */ 23 | public PeopleService(Driver driver) { 24 | this.driver = driver; 25 | this.people = AppUtils.loadFixtureList("people"); 26 | } 27 | 28 | /** 29 | * This method should return a paginated list of People (actors or directors), 30 | * with an optional filter on the person's name based on the `q` parameter. 31 | * 32 | * Results should be ordered by the `sort` parameter and limited to the 33 | * number passed as `limit`. The `skip` variable should be used to skip a 34 | * certain number of rows. 35 | * 36 | * @param params Used to filter on the person's name, and query parameters for pagination and ordering 37 | * @return List 38 | */ 39 | // tag::all[] 40 | public List> all(Params params) { 41 | // TODO: Get a list of people from the database 42 | if (params.query() != null) { 43 | return AppUtils.process(people.stream() 44 | .filter(p -> ((String)p.get("name")) 45 | .contains(params.query())) 46 | .toList(), params); 47 | } 48 | return AppUtils.process(people, params); 49 | } 50 | // end::all[] 51 | 52 | /** 53 | * Find a user by their ID. 54 | * 55 | * If no user is found, a NotFoundError should be thrown. 56 | * 57 | * @param id The tmdbId for the user 58 | * @return Person 59 | */ 60 | // tag::findById[] 61 | public Map findById(String id) { 62 | // TODO: Find a user by their ID 63 | 64 | return people.stream().filter(p -> id.equals(p.get("tmdbId"))).findAny().get(); 65 | } 66 | // end::findById[] 67 | 68 | /** 69 | * Get a list of similar people to a Person, ordered by their similarity score 70 | * in descending order. 71 | * 72 | * @param id The ID of the user 73 | * @param params Query parameters for pagination and ordering 74 | * @return List similar people 75 | */ 76 | // tag::getSimilarPeople[] 77 | public List> getSimilarPeople(String id, Params params) { 78 | // TODO: Get a list of similar people to the person by their id 79 | 80 | return AppUtils.process(people, params); 81 | } 82 | // end::getSimilarPeople[] 83 | 84 | } -------------------------------------------------------------------------------- /src/main/java/neoflix/routes/MovieRoutes.java: -------------------------------------------------------------------------------- 1 | package neoflix.routes; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import io.javalin.apibuilder.EndpointGroup; 6 | import neoflix.Params; 7 | import neoflix.AppUtils; 8 | import neoflix.services.MovieService; 9 | import neoflix.services.RatingService; 10 | import org.neo4j.driver.Driver; 11 | 12 | import java.util.Map; 13 | 14 | import static io.javalin.apibuilder.ApiBuilder.get; 15 | 16 | public class MovieRoutes implements EndpointGroup { 17 | private final Gson gson; 18 | private final MovieService movieService; 19 | private final RatingService ratingService; 20 | 21 | public MovieRoutes(Driver driver, Gson gson) { 22 | this.gson = gson; 23 | // tag::list[] 24 | movieService = new MovieService(driver); // <1> 25 | // end::list[] 26 | ratingService = new RatingService(driver); 27 | } 28 | 29 | @Override 30 | public void addEndpoints() { 31 | /* 32 | * @GET /movies 33 | * 34 | * This route should return a paginated list of movies, sorted by the 35 | * `sort` query parameter, 36 | */ 37 | // tag::list[] 38 | get("", ctx -> { 39 | var params = Params.parse(ctx, Params.MOVIE_SORT); // <2> 40 | String userId = AppUtils.getUserId(ctx); // <3> 41 | var movies = movieService.all(params, userId); // <4> 42 | ctx.result(gson.toJson(movies)); 43 | }); 44 | // end::list[] 45 | 46 | /* 47 | * @GET /movies/{id} 48 | * 49 | * This route should find a movie by its tmdbId and return its properties. 50 | */ 51 | // tag::get[] 52 | get("/{id}", ctx -> { 53 | String userId = AppUtils.getUserId(ctx); 54 | Map movie = movieService.findById(ctx.pathParam("id"), userId); 55 | ctx.result(gson.toJson(movie)); 56 | }); 57 | 58 | /* 59 | * @GET /movies/{id}/ratings 60 | * 61 | * 62 | * This route should return a paginated list of ratings for a movie, ordered by either 63 | * the rating itself or when the review was created. 64 | */ 65 | // tag::ratings[] 66 | get("/{id}/ratings", ctx -> ctx.result(gson.toJson(ratingService.forMovie(ctx.pathParam("id"), Params.parse(ctx, Params.RATING_SORT))))); 67 | // end::ratings[] 68 | 69 | /* 70 | * @GET /movies/{id}/similar 71 | * 72 | * This route should return a paginated list of similar movies, ordered by the 73 | * similarity score in descending order. 74 | */ 75 | // tag::similar[] 76 | get("/{id}/similar", ctx -> { 77 | var userId = AppUtils.getUserId(ctx); 78 | var movies = movieService.getSimilarMovies(ctx.pathParam("id"), Params.parse(ctx, Params.MOVIE_SORT), userId); 79 | ctx.result(gson.toJson(movies)); 80 | }); 81 | // end::similar[] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/goodfellas.json: -------------------------------------------------------------------------------- 1 | { 2 | "actors": [ 3 | { 4 | "name": "Joe Pesci", 5 | "bornIn": "Newark, New Jersey, USA ", 6 | "tmdbId": "0000582", 7 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/7ecSqd7GXYbK3sJw1lvLWLiJ6fh.jpg", 8 | "born": "1943-02-09" 9 | }, 10 | { 11 | "name": "Lorraine Bracco", 12 | "bornIn": "Bay Ridge - Brooklyn - New York City - New York - USA", 13 | "tmdbId": "0000966", 14 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/1lQiN8yggIJ8aGYLp4Nul3ALdXC.jpg", 15 | "born": "1954-10-02" 16 | }, 17 | { 18 | "name": "Ray Liotta", 19 | "bornIn": "Newark, New Jersey, USA", 20 | "tmdbId": "0000501", 21 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/4trMXwGW6OZpyvDYQ7a5ZCxk9KL.jpg", 22 | "born": "1954-12-18" 23 | }, 24 | { 25 | "name": "Robert De Niro", 26 | "bornIn": "Greenwich Village, New York City, New York, USA", 27 | "tmdbId": "0000134", 28 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/cT8htcckIuyI1Lqwt1CvD02ynTh.jpg", 29 | "born": "1943-08-17" 30 | } 31 | ], 32 | "plot": "Henry Hill and his friends work their way up through the mob hierarchy.", 33 | "year": 1990, 34 | "genres": [ 35 | { 36 | "link": "/genres/Crime", 37 | "name": "Crime" 38 | }, 39 | { 40 | "link": "/genres/Drama", 41 | "name": "Drama" 42 | } 43 | ], 44 | "runtime": 146, 45 | "directors": [ 46 | { 47 | "name": "Martin Scorsese", 48 | "bornIn": "Queens, New York, USA", 49 | "tmdbId": "0000217", 50 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/9U9Y5GQuWX3EZy39B8nkk4NY01S.jpg", 51 | "born": "1942-11-17" 52 | } 53 | ], 54 | "imdbRating": 8.7, 55 | "languages": [ 56 | "English", 57 | "Italian" 58 | ], 59 | "tmdbId": "769", 60 | "title": "Goodfellas", 61 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/oErEczcVUmJm0EPdvWsvK4g4Lv3.jpg", 62 | "ratingCount": 124, 63 | "ratings": [ 64 | { 65 | "imdbRating": 2.0, 66 | "user": { 67 | "name": "Catherine Trujillo", 68 | "tmdbId": "570" 69 | }, 70 | "timestamp": 1475784311 71 | }, 72 | { 73 | "imdbRating": 5.0, 74 | "user": { 75 | "name": "Teresa Graham", 76 | "tmdbId": "457" 77 | }, 78 | "timestamp": 1471383372 79 | }, 80 | { 81 | "imdbRating": 5.0, 82 | "user": { 83 | "name": "Meredith Leonard", 84 | "tmdbId": "519" 85 | }, 86 | "timestamp": 1471150621 87 | }, 88 | { 89 | "imdbRating": 4.0, 90 | "user": { 91 | "name": "Dr. Angela Johnson", 92 | "tmdbId": "56" 93 | }, 94 | "timestamp": 1467003139 95 | }, 96 | { 97 | "imdbRating": 5.0, 98 | "user": { 99 | "name": "Melissa King", 100 | "tmdbId": "483" 101 | }, 102 | "timestamp": 1465387394 103 | } 104 | ] 105 | } -------------------------------------------------------------------------------- /src/test/java/neoflix/_05_AuthenticationTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.AuthService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Values; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class _05_AuthenticationTest { 13 | private static Driver driver; 14 | private static String jwtSecret; 15 | 16 | private static final String email = "authenticated@neo4j.com"; 17 | private static final String password = "AuthenticateM3!"; 18 | private static final String name = "Authenticated User"; 19 | 20 | @BeforeAll 21 | static void initDriverAuth() { 22 | AppUtils.loadProperties(); 23 | driver = AppUtils.initDriver(); 24 | jwtSecret = AppUtils.getJwtSecret(); 25 | 26 | if (driver != null) driver.session().executeWrite(tx -> tx.run("MATCH (u:User {email: $email}) DETACH DELETE u", Values.parameters("email", email))); 27 | } 28 | 29 | @AfterAll 30 | static void closeDriver() { 31 | if (driver != null) driver.close(); 32 | } 33 | 34 | @Test 35 | void authenticateUser() { 36 | AuthService authService = new AuthService(driver, jwtSecret); 37 | authService.register(email, password, name); 38 | 39 | var output = authService.authenticate(email, password); 40 | assertEquals(email, output.get("email"), "email property"); 41 | assertEquals(name, output.get("name"), "name property"); 42 | assertNotNull(output.get("token"), "token property generated"); 43 | assertNotNull(output.get("userId"), "userId property generated"); 44 | assertNull(output.get("password"), "no password returned"); 45 | 46 | setUserAuthTestTimestamp(); 47 | } 48 | 49 | @Test 50 | void tryAuthWithIncorrectPassword() { 51 | AuthService authService = new AuthService(driver, jwtSecret); 52 | 53 | try { 54 | authService.authenticate(email, "unknown"); 55 | fail("incorrect password auth should fail"); 56 | } catch (Exception e) { 57 | assertEquals("Incorrect email", e.getMessage()); 58 | } 59 | } 60 | 61 | @Test 62 | void tryAuthWithIncorrectUsername() { 63 | AuthService authService = new AuthService(driver, jwtSecret); 64 | 65 | try { 66 | authService.authenticate("unknown", "unknown"); 67 | fail("Auth with unknown username should fail"); 68 | } catch (Exception e) { 69 | assertEquals("Incorrect email", e.getMessage()); 70 | } 71 | } 72 | 73 | void setUserAuthTestTimestamp() { 74 | if (driver != null) try (var session = driver.session()) { 75 | session.executeWrite(tx -> { 76 | tx.run(""" 77 | MATCH (u:User {email: $email}) 78 | SET u.authenticatedAt = datetime() 79 | """, Values.parameters("email", email)); 80 | return null; 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/genres.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "movies": 1545, 4 | "name": "Action", 5 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/qJ2tW6WMUDux911r6m7haRef0WH.jpg" 6 | }, 7 | { 8 | "movies": 1117, 9 | "name": "Adventure", 10 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/rCzpDGLbOoPwLjy3OAm5NUPOTrC.jpg" 11 | }, 12 | { 13 | "movies": 447, 14 | "name": "Animation", 15 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/eENI0WN2AAuQWfPmQupzMD6G4gV.jpg" 16 | }, 17 | { 18 | "movies": 583, 19 | "name": "Children", 20 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/bSqt9rhDZx1Q7UZ86dBPKdNomp2.jpg" 21 | }, 22 | { 23 | "movies": 3315, 24 | "name": "Comedy", 25 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/vnUzbdtqkudKSBgX0KGivfpdYNB.jpg" 26 | }, 27 | { 28 | "movies": 1100, 29 | "name": "Crime", 30 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/5KCVkau1HEl7ZzfPsKAPM0sMiKc.jpg" 31 | }, 32 | { 33 | "movies": 495, 34 | "name": "Documentary", 35 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/gVVd7hEfOgJ3OYkOUaoCqIZMmpC.jpg" 36 | }, 37 | { 38 | "movies": 4365, 39 | "name": "Drama", 40 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/5KCVkau1HEl7ZzfPsKAPM0sMiKc.jpg" 41 | }, 42 | { 43 | "movies": 654, 44 | "name": "Fantasy", 45 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/rCzpDGLbOoPwLjy3OAm5NUPOTrC.jpg" 46 | }, 47 | { 48 | "movies": 133, 49 | "name": "Film-Noir", 50 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/zt8aQ6ksqK6p1AopC5zVTDS9pKT.jpg" 51 | }, 52 | { 53 | "movies": 877, 54 | "name": "Horror", 55 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/qjWV4Aq4t0SMhuRpkJ4q3D6byXq.jpg" 56 | }, 57 | { 58 | "movies": 153, 59 | "name": "IMAX", 60 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/qJ2tW6WMUDux911r6m7haRef0WH.jpg" 61 | }, 62 | { 63 | "movies": 394, 64 | "name": "Musical", 65 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/gVVd7hEfOgJ3OYkOUaoCqIZMmpC.jpg" 66 | }, 67 | { 68 | "movies": 543, 69 | "name": "Mystery", 70 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/9gk7adHYeDvHkCSEqAvQNLV5Uge.jpg" 71 | }, 72 | { 73 | "movies": 1545, 74 | "name": "Romance", 75 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/clolk7rB5lAjs41SD0Vt6IXYLMm.jpg" 76 | }, 77 | { 78 | "movies": 792, 79 | "name": "Sci-Fi", 80 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/fR0VZ0VE598zl1lrYf7IfBqEwQ2.jpg" 81 | }, 82 | { 83 | "movies": 1729, 84 | "name": "Thriller", 85 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/wR5HZWdVpcXx9sevV1bQi7rP4op.jpg" 86 | }, 87 | { 88 | "movies": 367, 89 | "name": "War", 90 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/c8Ass7acuOe4za6DhSattE359gr.jpg" 91 | }, 92 | { 93 | "movies": 168, 94 | "name": "Western", 95 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/eWivEg4ugIMAd7d4uWI37b17Cgj.jpg" 96 | } 97 | ] -------------------------------------------------------------------------------- /src/test/java/neoflix/_04_ConstraintErrorTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.AuthService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.Assumptions; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | import org.neo4j.driver.Driver; 9 | import org.neo4j.driver.Values; 10 | 11 | import java.util.UUID; 12 | 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | class _04_ConstraintErrorTest { 16 | private static Driver driver; 17 | private static String jwtSecret; 18 | 19 | private static final String email = UUID.randomUUID() +"@neo4j.com"; 20 | private static final String password = UUID.randomUUID().toString(); 21 | private static final String name = "Graph Academy"; 22 | 23 | @BeforeAll 24 | static void initDriverAuth() { 25 | AppUtils.loadProperties(); 26 | driver = AppUtils.initDriver(); 27 | jwtSecret = AppUtils.getJwtSecret(); 28 | } 29 | 30 | @AfterAll 31 | static void closeDriver() { 32 | if (driver != null) { 33 | driver.session().executeWrite(tx -> tx.run("MATCH (u:User {email: $email}) DETACH DELETE u", Values.parameters("email", email))); 34 | driver.close(); 35 | } 36 | } 37 | 38 | /* 39 | * If this error fails, try running the following query in your Sandbox to create the unique constraint 40 | * CREATE CONSTRAINT UserEmailUnique FOR ( user:User ) REQUIRE (user.email) IS UNIQUE 41 | */ 42 | @Test 43 | void findUniqueConstraint() { 44 | Assumptions.assumeTrue(driver != null); 45 | try (var session = driver.session()) { 46 | session.executeRead(tx -> { 47 | var constraint = tx.run(""" 48 | SHOW CONSTRAINTS 49 | YIELD name, type, labelsOrTypes, properties 50 | WHERE type = 'UNIQUENESS' 51 | AND 'User' IN labelsOrTypes 52 | AND 'email' IN properties 53 | RETURN name 54 | """); 55 | assertNotNull(constraint); 56 | assertEquals(1, constraint.stream().count(), "Found unique constraint"); 57 | return null; 58 | }); 59 | } 60 | } 61 | 62 | @Test 63 | void checkConstraintWithDuplicateUser() { 64 | AuthService authService = new AuthService(driver, jwtSecret); 65 | var output = authService.register(email, password, name); 66 | 67 | assertEquals(email, output.get("email"), "email property"); 68 | assertEquals(name, output.get("name"), "name property"); 69 | assertNotNull(output.get("token"), "token property generated"); 70 | assertNotNull(output.get("userId"), "userId property generated"); 71 | assertNull(output.get("password"), "no password returned"); 72 | 73 | //Retry with same credentials 74 | try { 75 | authService.register(email, password, name); 76 | fail("Retry should fail"); 77 | } catch (Exception e) { 78 | assertEquals("An account already exists with the email address", e.getMessage()); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/resources/public/js/genres.list.bb023af3.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["genres.list"],{"3d87":function(e,t,c){"use strict";var n=c("7a23"),r={class:"section section--head"},o={class:"container"},a={class:"row"},s={class:"col-12"},i={class:"section__title section__title--head"};function b(e,t,c,b,l,j){return Object(n["u"])(),Object(n["f"])("section",r,[Object(n["g"])("div",o,[Object(n["g"])("div",a,[Object(n["g"])("div",s,[Object(n["g"])("h1",i,Object(n["E"])(e.title),1)])])])])}var l=Object(n["k"])({props:{title:String}}),j=c("6b0d"),u=c.n(j);const O=u()(l,[["render",b]]);t["a"]=O},"43f4":function(e,t,c){"use strict";c.d(t,"b",(function(){return r})),c.d(t,"a",(function(){return o}));var n=c("5e73");function r(){return Object(n["c"])("/genres")}function o(e){return Object(n["c"])("/genres/".concat(e))}},"5b53":function(e,t,c){"use strict";c.r(t);c("b0c0");var n=c("7a23"),r={class:"container"},o={class:"row"};function a(e,t,c,a,s,i){var b=Object(n["C"])("Hero"),l=Object(n["C"])("placeholder"),j=Object(n["C"])("grid"),u=Object(n["C"])("genre");return Object(n["u"])(),Object(n["f"])(n["a"],null,[Object(n["j"])(b,{title:"Browse Genres"}),Object(n["g"])("div",r,[Object(n["g"])("div",o,[e.loading?(Object(n["u"])(),Object(n["d"])(j,{key:0},{default:Object(n["K"])((function(){return[Object(n["j"])(l),Object(n["j"])(l),Object(n["j"])(l),Object(n["j"])(l)]})),_:1})):(Object(n["u"])(),Object(n["d"])(j,{key:1},{default:Object(n["K"])((function(){return[(Object(n["u"])(!0),Object(n["f"])(n["a"],null,Object(n["A"])(e.genres,(function(e){return Object(n["u"])(),Object(n["d"])(u,{key:e.name,to:{name:"GenreView",params:{name:e.name}},poster:e.poster,name:e.name,movies:e.movies},null,8,["to","poster","name","movies"])})),128))]})),_:1}))])])],64)}var s=c("43f4"),i=c("3d87"),b=c("c2c0"),l={class:"col-12 col-sm-6 col-lg-4 col-xl-3"},j={class:"category__cover"},u=["src"],O={class:"category__title"},d={class:"category__value"};function g(e,t,c,r,o,a){var s=Object(n["C"])("router-link");return Object(n["u"])(),Object(n["f"])("div",l,[Object(n["j"])(s,{to:e.to,class:"category"},{default:Object(n["K"])((function(){return[Object(n["g"])("div",j,[Object(n["g"])("img",{src:e.posterImage,alt:""},null,8,u)]),Object(n["g"])("h3",O,Object(n["E"])(e.name),1),Object(n["g"])("span",d,Object(n["E"])(e.movies),1)]})),_:1},8,["to"])])}c("a9e3");var f=Object(n["k"])({name:"GenreGridItem",props:{to:Object,poster:String,name:String,movies:Number},computed:{posterImage:function(){return this.poster||"/img/poster-placeholder.png"}}}),m=c("6b0d"),v=c.n(m);const p=v()(f,[["render",g]]);var _=p,h=c("b71d"),k=Object(n["k"])({components:{Hero:i["a"],Grid:b["a"],Genre:_,Placeholder:h["a"]},setup:function(){var e=Object(s["b"])(),t=e.loading,c=e.data;return{loading:t,genres:c}}});const w=v()(k,[["render",a]]);t["default"]=w},b71d:function(e,t,c){"use strict";var n=c("7a23"),r={class:"col-12 col-sm-6 col-lg-4 col-xl-3 loading"},o=Object(n["g"])("div",{class:"category__cover"},[Object(n["g"])("img",{src:"/img/poster-placeholder.png",alt:""})],-1),a=Object(n["g"])("h3",{class:"category__title"},"Loading...",-1),s=[o,a];function i(e,t,c,o,a,i){return Object(n["u"])(),Object(n["f"])("div",r,s)}var b=Object(n["k"])({name:"GridPlaceholder"}),l=c("6b0d"),j=c.n(l);const u=j()(b,[["render",i]]);t["a"]=u}}]); 2 | //# sourceMappingURL=genres.list.bb023af3.js.map -------------------------------------------------------------------------------- /src/test/java/neoflix/_02_MovieListTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.MovieService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | 9 | import static neoflix.Params.Order.ASC; 10 | import static neoflix.Params.Order.DESC; 11 | import static neoflix.Params.Sort.*; 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | class _02_MovieListTest { 15 | 16 | private static Driver driver; 17 | 18 | @BeforeAll 19 | static void initDriver() { 20 | AppUtils.loadProperties(); 21 | driver = AppUtils.initDriver(); 22 | } 23 | 24 | @AfterAll 25 | static void closeDriver() { 26 | if (driver != null) driver.close(); 27 | } 28 | 29 | @Test 30 | void applyOrderListAndSkip() { 31 | MovieService movieService = new MovieService(driver); 32 | var limit = 1; 33 | var output = movieService.all(new Params(null, imdbRating, ASC, limit, 0), null); 34 | assertNotNull(output); 35 | assertEquals(limit, output.size()); 36 | assertNotNull(output.get(0)); 37 | var firstTitle = output.get(0).get("title"); 38 | assertNotNull(firstTitle); 39 | assertEquals("Ring of Terror", firstTitle); 40 | 41 | var skip = 1; 42 | var next = movieService.all(new Params(null, Params.Sort.imdbRating, ASC, limit, skip), null); 43 | assertNotNull(next); 44 | assertEquals(limit, next.size()); 45 | assertNotEquals(firstTitle, next.get(0).get("title")); 46 | } 47 | 48 | @Test 49 | void testSorting() { 50 | MovieService movieService = new MovieService(driver); 51 | var limit = 1; 52 | var byReleased = movieService.all(new Params(null, released, DESC, limit, 0), null); 53 | assertNotNull(byReleased); 54 | assertEquals(limit, byReleased.size()); 55 | assertNotNull(byReleased.get(0)); 56 | var releaseDate = byReleased.get(0).get("released"); 57 | assertNotNull(releaseDate); 58 | assertEquals("2016-09-02", releaseDate); 59 | 60 | var byRating = movieService.all(new Params(null, Params.Sort.imdbRating, DESC, limit, 0), null); 61 | assertNotNull(byRating); 62 | assertEquals(limit, byRating.size()); 63 | assertNotNull(byRating.get(0)); 64 | var rating = byRating.get(0).get("imdbRating"); 65 | assertNotNull(rating); 66 | assertEquals(9.6, rating); 67 | } 68 | 69 | @Test 70 | void orderMoviesByRating() { 71 | var movieService = new MovieService(driver); 72 | var limit = 1; 73 | var output = movieService.all(new Params(null, imdbRating, DESC, limit, 0), null); 74 | assertNotNull(output); 75 | assertEquals(limit, output.size()); 76 | assertNotNull(output.get(0)); 77 | var firstTitle = output.get(0).get("title"); 78 | assertNotNull(firstTitle); 79 | 80 | System.out.println(""" 81 | 82 | Here is the answer to the quiz question on the lesson: 83 | What is the title of the highest rated movie in the recommendations dataset? 84 | Copy and paste the following answer into the text box: 85 | """); 86 | System.out.println(firstTitle); 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/neoflix/AppUtils.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import com.google.gson.Gson; 4 | import org.neo4j.driver.AuthToken; 5 | import org.neo4j.driver.AuthTokens; 6 | import org.neo4j.driver.Driver; 7 | import org.neo4j.driver.GraphDatabase; 8 | 9 | import io.javalin.http.Context; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.function.Function; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | 19 | public class AppUtils { 20 | public static void loadProperties() { 21 | try { 22 | var file = AppUtils.class.getResourceAsStream("/application.properties"); 23 | if (file!=null) System.getProperties().load(file); 24 | } catch (IOException e) { 25 | throw new RuntimeException("Error loading application.properties", e); 26 | } 27 | } 28 | 29 | public static String getUserId(Context ctx) { 30 | Object user = ctx.attribute("user"); 31 | if (user == null) return null; 32 | return user.toString(); 33 | } 34 | 35 | static void handleAuthAndSetUser(HttpServletRequest request, String jwtSecret) { 36 | String token = request.getHeader("Authorization"); 37 | String bearer = "Bearer "; 38 | if (token != null && !token.isBlank() && token.startsWith(bearer)) { 39 | // verify token 40 | token = token.substring(bearer.length()); 41 | String userId = AuthUtils.verify(token, jwtSecret); 42 | request.setAttribute("user", userId); 43 | } 44 | } 45 | 46 | // tag::initDriver[] 47 | static Driver initDriver() { 48 | // TODO: Create and assign an instance of the driver here 49 | return null; 50 | } 51 | // end::initDriver[] 52 | 53 | static int getServerPort() { 54 | return Integer.parseInt(System.getProperty("APP_PORT", "3000")); 55 | } 56 | 57 | static String getJwtSecret() { 58 | return System.getProperty("JWT_SECRET"); 59 | } 60 | 61 | static String getNeo4jUri() { 62 | return System.getProperty("NEO4J_URI"); 63 | } 64 | static String getNeo4jUsername() { 65 | return System.getProperty("NEO4J_USERNAME"); 66 | } 67 | static String getNeo4jPassword() { 68 | return System.getProperty("NEO4J_PASSWORD"); 69 | } 70 | 71 | public static List> loadFixtureList(final String name) { 72 | var fixture = new InputStreamReader(AppUtils.class.getResourceAsStream("/fixtures/" + name + ".json")); 73 | return GsonUtils.gson().fromJson(fixture,List.class); 74 | } 75 | public static List> process(List> result, Params params) { 76 | return params == null ? result : result.stream() 77 | .sorted((m1, m2) -> 78 | (params.order() == Params.Order.ASC ? 1 : -1) * 79 | ((Comparable)m1.getOrDefault(params.sort().name(),"")).compareTo( 80 | m2.getOrDefault(params.sort().name(),"") 81 | )) 82 | .skip(params.skip()).limit(params.limit()) 83 | .toList(); 84 | } 85 | 86 | public static Map loadFixtureSingle(final String name) { 87 | var fixture = new InputStreamReader(AppUtils.class.getResourceAsStream("/fixtures/" + name + ".json")); 88 | return GsonUtils.gson().fromJson(fixture,Map.class); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/neoflix/routes/AccountRoutes.java: -------------------------------------------------------------------------------- 1 | package neoflix.routes; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import io.javalin.apibuilder.EndpointGroup; 6 | import neoflix.Params; 7 | import neoflix.AppUtils; 8 | import neoflix.services.FavoriteService; 9 | import neoflix.services.RatingService; 10 | import org.neo4j.driver.Driver; 11 | 12 | import java.util.Map; 13 | 14 | import static io.javalin.apibuilder.ApiBuilder.*; 15 | 16 | public class AccountRoutes implements EndpointGroup { 17 | private final Gson gson; 18 | private final FavoriteService favoriteService; 19 | private final RatingService ratingService; 20 | 21 | public AccountRoutes(Driver driver, Gson gson) { 22 | this.gson = gson; 23 | favoriteService = new FavoriteService(driver); 24 | ratingService = new RatingService(driver); 25 | } 26 | 27 | @Override 28 | public void addEndpoints() { 29 | /* 30 | * @GET /account/ 31 | * 32 | * This route simply returns the claims made in the JWT token 33 | */ 34 | get("", ctx -> ctx.result(gson.toJson(ctx.attribute("user")))); 35 | 36 | /* 37 | * @GET /account/favorites/ 38 | * 39 | * This route should return a list of movies that a user has added to their 40 | * Favorites link by clicking the Bookmark icon on a Movie card. 41 | */ 42 | // tag::list[] 43 | get("/favorites", ctx -> { 44 | var userId = AppUtils.getUserId(ctx); 45 | var favorites = favoriteService.all(userId, Params.parse(ctx, Params.MOVIE_SORT)); 46 | ctx.result(gson.toJson(favorites)); 47 | }); 48 | // end::list[] 49 | 50 | /* 51 | * @POST /account/favorites/{id} 52 | * 53 | * This route should create a `:HAS_FAVORITE` relationship between the current user 54 | * and the movie with the {id} parameter. 55 | */ 56 | // tag::add[] 57 | post("/favorites/{id}", ctx -> { 58 | var userId = AppUtils.getUserId(ctx); 59 | var newFavorite = favoriteService.add(userId, ctx.pathParam("id")); 60 | ctx.result(gson.toJson(newFavorite)); 61 | }); 62 | // end::add[] 63 | 64 | /* 65 | * @DELETE /account/favorites/{id} 66 | * 67 | * This route should remove the `:HAS_FAVORITE` relationship between the current user 68 | * and the movie with the {id} parameter. 69 | */ 70 | // tag::delete[] 71 | delete("/favorites/{id}", ctx -> { 72 | var userId = AppUtils.getUserId(ctx); // TODO 73 | var deletedFavorite = favoriteService.remove(userId, ctx.pathParam("id")); 74 | ctx.result(gson.toJson(deletedFavorite)); 75 | }); 76 | // end::delete[] 77 | 78 | /* 79 | * @POST /account/ratings/{id} 80 | * 81 | * This route should create a `:RATING` relationship between the current user 82 | * and the movie with the {id} parameter. The rating value will be posted as part 83 | * of the post body {"rating": "5"}. 84 | */ 85 | // tag::rating[] 86 | post("/ratings/{id}", ctx -> { 87 | var userId = AppUtils.getUserId(ctx); // TODO 88 | var value = Integer.parseInt(gson.fromJson(ctx.body(), Map.class).get("rating").toString()); 89 | var rating = ratingService.add(userId, ctx.pathParam("id"), value); 90 | ctx.result(gson.toJson(rating)); 91 | }); 92 | // end::rating[] 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.example 8 | app-java 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 17 13 | UTF-8 14 | 15 | 16 | 17 | 18 | io.javalin 19 | javalin 20 | 4.6.4 21 | 22 | 23 | org.slf4j 24 | slf4j-simple 25 | 1.7.31 26 | 27 | 28 | 29 | org.neo4j.driver 30 | neo4j-java-driver 31 | 5.1.0 32 | 33 | 34 | 35 | com.google.code.gson 36 | gson 37 | 2.8.9 38 | 39 | 40 | com.auth0 41 | java-jwt 42 | 3.19.2 43 | 44 | 45 | at.favre.lib 46 | bcrypt 47 | 0.9.0 48 | 49 | 50 | 51 | io.projectreactor 52 | reactor-core 53 | 3.4.21 54 | true 55 | 56 | 57 | org.junit.jupiter 58 | junit-jupiter 59 | 5.8.2 60 | test 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | java 70 | 71 | 72 | 73 | org.codehaus.mojo 74 | exec-maven-plugin 75 | 3.0.0 76 | 77 | neoflix.NeoflixApp 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-surefire-plugin 83 | 3.0.0-M5 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-compiler-plugin 88 | 3.10.1 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/main/resources/public/js/genres.view.78be501f.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["genres.view"],{4222:function(e,t,c){"use strict";c.r(t);c("b0c0"),c("4e82");var n=c("7a23"),r={key:0},o={key:1},a={class:"catalog"},i={class:"container"},b={class:"row"},d={class:"col-12"},l={class:"catalog__nav"},j={class:"slider-radio"},s=["onChange","value","id","checked"],u=["for"],O={key:2,class:"row"},g={class:"col-12"};function v(e,t,c,v,f,m){var p=Object(n["C"])("Section"),k=Object(n["C"])("placeholder"),y=Object(n["C"])("grid"),w=Object(n["C"])("MovieGridItem");return e.genreLoading?(Object(n["u"])(),Object(n["f"])("div",r,[Object(n["j"])(p,{title:"Loading",to:"/genres"})])):(Object(n["u"])(),Object(n["f"])("div",o,[Object(n["j"])(p,{title:e.genre.name,to:{name:"GenreView",props:e.genre},background:e.genre.poster},null,8,["title","to","background"]),Object(n["g"])("div",a,[Object(n["g"])("div",i,[Object(n["g"])("div",b,[Object(n["g"])("div",d,[Object(n["g"])("div",l,[Object(n["g"])("div",j,[(Object(n["u"])(!0),Object(n["f"])(n["a"],null,Object(n["A"])(e.orderBy,(function(t){return Object(n["u"])(),Object(n["f"])(n["a"],{key:t.value},[Object(n["g"])("input",{name:"sort",onChange:Object(n["M"])((function(c){return e.setSort(t.value)}),["prevent"]),type:"radio",value:t.value,id:t.value,checked:t.value===e.sort},null,40,s),Object(n["g"])("label",{for:t.value},Object(n["E"])(t.label),9,u)],64)})),128))])])])]),e.moviesLoading?(Object(n["u"])(),Object(n["d"])(y,{key:0},{default:Object(n["K"])((function(){return[Object(n["j"])(k),Object(n["j"])(k),Object(n["j"])(k),Object(n["j"])(k),Object(n["j"])(k),Object(n["j"])(k)]})),_:1})):(Object(n["u"])(),Object(n["d"])(y,{key:1},{default:Object(n["K"])((function(){return[(Object(n["u"])(!0),Object(n["f"])(n["a"],null,Object(n["A"])(e.movies,(function(e){return Object(n["u"])(),Object(n["d"])(w,{key:e.tmdbId,to:{name:"MovieView",params:{tmdbId:e.tmdbId}},tmdbId:e.tmdbId,title:e.title,imdbRating:e.imdbRating,rating:e.rating,poster:e.poster,list:e.genres,favorite:e.favorite},null,8,["to","tmdbId","title","imdbRating","rating","poster","list","favorite"])})),128))]})),_:1})),e.more?(Object(n["u"])(),Object(n["f"])("div",O,[Object(n["g"])("div",g,[Object(n["g"])("button",{class:"catalog__more",type:"button",onClick:t[0]||(t[0]=function(){return e.loadMore()})},"Load more")])])):Object(n["e"])("",!0)])])]))}var f=c("b71d"),m=c("43f4"),p=c("e490"),k=c("8317"),y=c("c2c0"),w=c("efbf"),h=c("6c02"),_=Object(n["k"])({components:{Section:k["a"],Grid:y["a"],MovieGridItem:w["a"],Placeholder:f["a"]},setup:function(){var e=Object(h["c"])(),t=e.params,c=Object(m["a"])(t.name),n=c.loading,r=c.data,o=Object(p["j"])(t.name,p["d"]),a=o.loading,i=o.data,b=o.more,d=o.loadMore,l=o.sort,j=o.setSort;return{genreLoading:n,genre:r,moviesLoading:a,movies:i,loadMore:d,more:b,orderBy:p["a"],sort:l,setSort:j}}}),I=c("6b0d"),C=c.n(I);const L=C()(_,[["render",v]]);t["default"]=L},"43f4":function(e,t,c){"use strict";c.d(t,"b",(function(){return r})),c.d(t,"a",(function(){return o}));var n=c("5e73");function r(){return Object(n["c"])("/genres")}function o(e){return Object(n["c"])("/genres/".concat(e))}},b71d:function(e,t,c){"use strict";var n=c("7a23"),r={class:"col-12 col-sm-6 col-lg-4 col-xl-3 loading"},o=Object(n["g"])("div",{class:"category__cover"},[Object(n["g"])("img",{src:"/img/poster-placeholder.png",alt:""})],-1),a=Object(n["g"])("h3",{class:"category__title"},"Loading...",-1),i=[o,a];function b(e,t,c,o,a,b){return Object(n["u"])(),Object(n["f"])("div",r,i)}var d=Object(n["k"])({name:"GridPlaceholder"}),l=c("6b0d"),j=c.n(l);const s=j()(d,[["render",b]]);t["a"]=s}}]); 2 | //# sourceMappingURL=genres.view.78be501f.js.map -------------------------------------------------------------------------------- /diff/03-registering-a-user.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/AuthService.java b/src/main/java/neoflix/services/AuthService.java 2 | index a5e479a..d8a5487 100644 3 | --- a/src/main/java/neoflix/services/AuthService.java 4 | +++ b/src/main/java/neoflix/services/AuthService.java 5 | @@ -4,6 +4,7 @@ import neoflix.AppUtils; 6 | import neoflix.AuthUtils; 7 | import neoflix.ValidationException; 8 | import org.neo4j.driver.Driver; 9 | +import org.neo4j.driver.Values; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | @@ -46,23 +47,33 @@ public class AuthService { 14 | // tag::register[] 15 | public Map register(String email, String plainPassword, String name) { 16 | var encrypted = AuthUtils.encryptPassword(plainPassword); 17 | - // tag::constraintError[] 18 | - // TODO: Handle Unique constraints in the database 19 | - var foundUser = users.stream().filter(u -> u.get("email").equals(email)).findAny(); 20 | - if (foundUser.isPresent()) { 21 | - throw new RuntimeException("An account already exists with the email address"); 22 | - } 23 | - // end::constraintError[] 24 | - 25 | - // TODO: Save user in database 26 | - var user = Map.of("email",email, "name",name, 27 | - "userId", String.valueOf(email.hashCode()), "password", encrypted); 28 | - users.add(user); 29 | + // Open a new Session 30 | + try (var session = this.driver.session()) { 31 | + // tag::create[] 32 | + var user = session.executeWrite(tx -> { 33 | + String statement = """ 34 | + CREATE (u:User { 35 | + userId: randomUuid(), 36 | + email: $email, 37 | + password: $encrypted, 38 | + name: $name 39 | + }) 40 | + RETURN u { .userId, .name, .email } as u"""; 41 | + var res = tx.run(statement, Values.parameters("email", email, "encrypted", encrypted, "name", name)); 42 | + // end::create[] 43 | + // tag::extract[] 44 | + // Extract safe properties from the user node (`u`) in the first row 45 | + return res.single().get("u").asMap(); 46 | + // end::extract[] 47 | 48 | - String sub = (String) user.get("userId"); 49 | - String token = AuthUtils.sign(sub,userToClaims(user), jwtSecret); 50 | + }); 51 | + String sub = (String)user.get("userId"); 52 | + String token = AuthUtils.sign(sub,userToClaims(user), jwtSecret); 53 | 54 | - return userWithToken(user, token); 55 | + // tag::return-register[] 56 | + return userWithToken(user, token); 57 | + // end::return-register[] 58 | + } 59 | } 60 | // end::register[] 61 | 62 | @@ -93,8 +104,8 @@ public class AuthService { 63 | if (foundUser.isEmpty()) 64 | throw new ValidationException("Incorrect email", Map.of("email","Incorrect email")); 65 | var user = foundUser.get(); 66 | - if (!plainPassword.equals(user.get("password")) && 67 | - !AuthUtils.verifyPassword(plainPassword,(String)user.get("password"))) { // 68 | + if (!plainPassword.equals(user.get("password")) && 69 | + !AuthUtils.verifyPassword(plainPassword,(String)user.get("password"))) { // 70 | throw new ValidationException("Incorrect password", Map.of("password","Incorrect password")); 71 | } 72 | // tag::return[] 73 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/similar.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "languages": [ 4 | "English" 5 | ], 6 | "plot": "A twisted take on 'Little Red Riding Hood' with a teenage juvenile delinquent on the run from a social worker traveling to her grandmother's house and being hounded by a charming, but sadistic, serial killer/pedophile.", 7 | "year": 1996, 8 | "genres": [ 9 | { 10 | "link": "/genres/Drama", 11 | "name": "Drama" 12 | }, 13 | { 14 | "link": "/genres/Crime", 15 | "name": "Crime" 16 | }, 17 | { 18 | "link": "/genres/Comedy", 19 | "name": "Comedy" 20 | }, 21 | { 22 | "link": "/genres/Thriller", 23 | "name": "Thriller" 24 | } 25 | ], 26 | "imdbRating": 6.9, 27 | "tmdbId": "0116361", 28 | "title": "Freeway", 29 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/m0pAARUq3foDWFsrUmlYDHtNPE9.jpg" 30 | }, 31 | { 32 | "languages": [ 33 | "English" 34 | ], 35 | "plot": "Atticus Finch, a lawyer in the Depression-era South, defends a black man against an undeserved rape charge, and his kids against prejudice.", 36 | "year": 1962, 37 | "genres": [ 38 | { 39 | "link": "/genres/Drama", 40 | "name": "Drama" 41 | } 42 | ], 43 | "imdbRating": 8.4, 44 | "tmdbId": "0056592", 45 | "title": "To Kill a Mockingbird", 46 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/ymbVkjMBqRFNJsxDUKXR27Kqsxa.jpg" 47 | }, 48 | { 49 | "languages": [ 50 | "German", 51 | " English", 52 | " French", 53 | " Turkish", 54 | " Hebrew", 55 | " Spanish", 56 | " Japanese" 57 | ], 58 | "plot": "An angel tires of overseeing human activity and wishes to become human when he falls in love with a mortal.", 59 | "year": 1987, 60 | "genres": [ 61 | { 62 | "link": "/genres/Drama", 63 | "name": "Drama" 64 | }, 65 | { 66 | "link": "/genres/Romance", 67 | "name": "Romance" 68 | }, 69 | { 70 | "link": "/genres/Fantasy", 71 | "name": "Fantasy" 72 | } 73 | ], 74 | "imdbRating": 8.1, 75 | "tmdbId": "0093191", 76 | "title": "Wings of Desire (Himmel über Berlin, Der)", 77 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/iZQs2vUeCzvS1KfZJ6uYNCGJBBV.jpg" 78 | }, 79 | { 80 | "languages": [ 81 | "English" 82 | ], 83 | "plot": "Wallace is used by a criminal penguin in a robbery involving mechanical trousers.", 84 | "year": 1993, 85 | "genres": [ 86 | { 87 | "link": "/genres/Comedy", 88 | "name": "Comedy" 89 | }, 90 | { 91 | "link": "/genres/Children", 92 | "name": "Children" 93 | }, 94 | { 95 | "link": "/genres/Animation", 96 | "name": "Animation" 97 | }, 98 | { 99 | "link": "/genres/Crime", 100 | "name": "Crime" 101 | } 102 | ], 103 | "imdbRating": 8.4, 104 | "tmdbId": "0108598", 105 | "title": "Wallace & Gromit: The Wrong Trousers", 106 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/wRTCxYHx1d9diFFmOHQZT7CjdUV.jpg" 107 | }, 108 | { 109 | "languages": [ 110 | "English" 111 | ], 112 | "plot": "A hack screenwriter writes a screenplay for a former silent-film star who has faded into Hollywood obscurity.", 113 | "year": 1950, 114 | "genres": [ 115 | { 116 | "link": "/genres/Romance", 117 | "name": "Romance" 118 | }, 119 | { 120 | "link": "/genres/Film-Noir", 121 | "name": "Film-Noir" 122 | }, 123 | { 124 | "link": "/genres/Drama", 125 | "name": "Drama" 126 | } 127 | ], 128 | "imdbRating": 8.5, 129 | "tmdbId": "0043014", 130 | "title": "Sunset Blvd. (a.k.a. Sunset Boulevard)", 131 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/zt8aQ6ksqK6p1AopC5zVTDS9pKT.jpg" 132 | } 133 | ] 134 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_07_FavoritesListTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.FavoriteService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.neo4j.driver.Driver; 9 | import org.neo4j.driver.Values; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class _07_FavoritesListTest { 14 | private static Driver driver; 15 | 16 | private static final String toyStory = "862"; 17 | private static final String goodfellas = "769"; 18 | private static final String userId = "9f965bf6-7e32-4afb-893f-756f502b2c2a"; 19 | private static final String email = "graphacademy.favorite@neo4j.com"; 20 | 21 | @BeforeAll 22 | static void initDriver() { 23 | AppUtils.loadProperties(); 24 | driver = AppUtils.initDriver(); 25 | if (driver != null) { 26 | try (var session = driver.session()) { 27 | session.executeWrite(tx -> tx.run(""" 28 | MERGE (u:User {userId: $userId}) SET u.email = $email 29 | """, Values.parameters("userId", userId, "email", email))); 30 | } 31 | } 32 | } 33 | 34 | @AfterAll 35 | static void closeDriver() { 36 | if (driver!=null) driver.close(); 37 | } 38 | 39 | @Test 40 | void notFoundIfMovieOrUserNotExist() { 41 | FavoriteService favoriteService = new FavoriteService(driver); 42 | 43 | try { 44 | favoriteService.add("unknown", "x999"); 45 | fail("Adding favorite with unknown userId or movieId should fail"); 46 | } catch (Exception e) { 47 | assertEquals("Couldn't create a favorite relationship for User unknown and Movie x999", e.getMessage()); 48 | } 49 | } 50 | 51 | @BeforeEach 52 | void removeFavorites() { 53 | if (driver != null) 54 | try (var session = driver.session()) { 55 | session.executeWrite(tx -> 56 | tx.run("MATCH (u:User {userId: $userId})-[r:HAS_FAVORITE]->(m:Movie) DELETE r", 57 | Values.parameters("userId", userId))); 58 | } 59 | } 60 | 61 | @Test 62 | void saveMovieToUserFavorites() { 63 | FavoriteService favoriteService = new FavoriteService(driver); 64 | 65 | var output = favoriteService.add(userId, toyStory); 66 | 67 | assertNotNull(output); 68 | assertEquals(toyStory, output.get("tmdbId")); 69 | assertTrue((Boolean)output.get("favorite"), "Toy Story is favorite"); 70 | 71 | var favorites = favoriteService.all(userId, new Params(null, Params.Sort.title, Params.Order.DESC, 10, 0)); 72 | 73 | var movieFavorite = favorites.stream().anyMatch(movie -> movie.get("tmdbId").equals(toyStory)); 74 | assertTrue(movieFavorite, "Toy Story is a favorite movie"); 75 | } 76 | 77 | @Test 78 | void addAndRemoveMovieFromFavorites() { 79 | FavoriteService favoriteService = new FavoriteService(driver); 80 | 81 | var add = favoriteService.add(userId, goodfellas); 82 | assertEquals(goodfellas, add.get("tmdbId")); 83 | assertTrue((Boolean)add.get("favorite"), "goodfellas is favorite"); 84 | 85 | var addCheck = favoriteService.all(userId, new Params(null, Params.Sort.title, Params.Order.DESC, 10, 0)); 86 | var found = addCheck.stream().anyMatch(movie -> movie.get("tmdbId").equals(goodfellas)); 87 | assertTrue(found, "goodfellas is a favorite"); 88 | 89 | var remove = favoriteService.remove(userId, goodfellas); 90 | assertEquals(goodfellas, remove.get("tmdbId")); 91 | assertEquals(false, remove.get("favorite"), "goodfellas is not a favorite anymore"); 92 | 93 | var removeCheck = favoriteService.all(userId, new Params(null, Params.Sort.title, Params.Order.DESC, 10, 0)); 94 | var notFound = removeCheck.stream().anyMatch(movie -> movie.get("tmdbId").equals(goodfellas)); 95 | assertFalse(notFound, "goodfellas is not a favorite anymore"); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /diff/12-movie-details.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/MovieService.java b/src/main/java/neoflix/services/MovieService.java 2 | index 0938190..c6e7b7a 100644 3 | --- a/src/main/java/neoflix/services/MovieService.java 4 | +++ b/src/main/java/neoflix/services/MovieService.java 5 | @@ -98,10 +98,32 @@ public class MovieService { 6 | */ 7 | // tag::findById[] 8 | public Map findById(String id, String userId) { 9 | - // TODO: Find a movie by its ID 10 | + // Find a movie by its ID 11 | // MATCH (m:Movie {tmdbId: $id}) 12 | 13 | - return popular.stream().filter(m -> id.equals(m.get("tmdbId"))).findAny().get(); 14 | + // Open a new database session 15 | + try (var session = this.driver.session()) { 16 | + // Find a movie by its ID 17 | + return session.executeRead(tx -> { 18 | + var favorites = getUserFavorites(tx, userId); 19 | + 20 | + String query = """ 21 | + MATCH (m:Movie {tmdbId: $id}) 22 | + RETURN m { 23 | + .*, 24 | + actors: [ (a)-[r:ACTED_IN]->(m) | a { .*, role: r.role } ], 25 | + directors: [ (d)-[:DIRECTED]->(m) | d { .* } ], 26 | + genres: [ (m)-[:IN_GENRE]->(g) | g { .name }], 27 | + ratingCount: size((m)<-[:RATED]-()), 28 | + favorite: m.tmdbId IN $favorites 29 | + } AS movie 30 | + LIMIT 1 31 | + """; 32 | + var res = tx.run(query, Values.parameters("id", id, "favorites", favorites)); 33 | + return res.single().get("movie").asMap(); 34 | + }); 35 | + 36 | + } 37 | } 38 | // end::findById[] 39 | 40 | @@ -125,14 +147,39 @@ public class MovieService { 41 | */ 42 | // tag::getSimilarMovies[] 43 | public List> getSimilarMovies(String id, Params params, String userId) { 44 | - // TODO: Get similar movies based on genres or ratings 45 | - 46 | - return AppUtils.process(popular, params).stream() 47 | - .map(item -> { 48 | - Map copy = new HashMap<>(item); 49 | - copy.put("score", ((int)(Math.random() * 10000)) / 100.0); 50 | - return copy; 51 | - }).toList(); 52 | + // Get similar movies based on genres or ratings 53 | + // MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m) 54 | + 55 | + // Open an Session 56 | + try (var session = this.driver.session()) { 57 | + 58 | + // Get similar movies based on genres or ratings 59 | + var movies = session.executeRead(tx -> { 60 | + var favorites = getUserFavorites(tx, userId); 61 | + String query = """ 62 | + MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m) 63 | + WHERE m.imdbRating IS NOT NULL 64 | + 65 | + WITH m, count(*) AS inCommon 66 | + WITH m, inCommon, m.imdbRating * inCommon AS score 67 | + ORDER BY score DESC 68 | + 69 | + SKIP $skip 70 | + LIMIT $limit 71 | + 72 | + RETURN m { 73 | + .*, 74 | + score: score, 75 | + favorite: m.tmdbId IN $favorites 76 | + } AS movie 77 | + """; 78 | + var res = tx.run(query, Values.parameters("id", id, "skip", params.skip(), "limit", params.limit(), "favorites", favorites)); 79 | + // Get a list of Movies from the Result 80 | + return res.list(row -> row.get("movie").asMap()); 81 | + }); 82 | + 83 | + return movies; 84 | + } 85 | } 86 | // end::getSimilarMovies[] 87 | 88 | -------------------------------------------------------------------------------- /src/main/resources/public/js/favorites.ff43e072.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/components/ui/grid/Column.vue","webpack:///./src/components/ui/grid/Column.vue?bd77","webpack:///./src/views/Favorites.vue","webpack:///./src/views/Favorites.vue?f20b"],"names":["class","script","__exports__","render","title","to","code","data","length","movie","key","tmdbId","imdbRating","rating","poster","list","genres","favorite","components","Section","Column","Grid","MovieGridItem","Movie","setup","loading","error"],"mappings":"8HACOA,MAAM,oC,wCAAX,eAEM,MAFN,EAEM,CADJ,eAAQ,sB,yBCDZ,MAAMC,EAAS,GAGTC,EAA2B,IAAgBD,EAAQ,CAAC,CAAC,SAASE,KAErD,U,6DCCT,eAA0D,SAAvD,uDAAmD,G,iBAGjB,W,iBAAqB,Q,iBAClB,Y,iBAAsB,kB,EAsB9D,eAAgD,SAA7C,6CAAyC,G,EAC5C,eAA2F,SAAxF,wFAAoF,G,iBAG1C,uB,iBAAiC,Q,iBAClC,iC,iBAA2C,M,sMArC3F,eAyCU,GAxCJC,MAAM,qBACNC,GAAG,W,yBAET,iBASO,CATS,MAAJ,EAAAC,M,iBAAZ,eASO,W,wBARL,iBAOS,CAPT,eAOS,Q,wBANP,iBAA0D,CAA1D,EAEA,eAGI,UAFF,eAAwD,GAA1CD,GAAI,gBAAe,C,wBAAE,iBAAO,C,cAC1C,eAA4D,GAA9CA,GAAI,mBAAkB,C,wBAAE,iBAAQ,C,qCAKnC,EAAAE,MAAQ,EAAAA,KAAKC,Q,iBAA9B,eAaO,W,wBAXH,iBAAqB,E,mBADvB,eAWE,2BAVgB,EAAAD,MAAI,SAAbE,G,wBADT,eAWE,GATCC,IAAKD,EAAME,OACXN,GAAE,iCAAuCI,EAAME,SAC/CA,OAAQF,EAAME,OACdP,MAAOK,EAAML,MACbQ,WAAYH,EAAMG,WAClBC,OAAQJ,EAAMI,OACdC,OAAQL,EAAMK,OACdC,KAAMN,EAAMO,OACZC,SAAUR,EAAMQ,U,uHAIrB,eAUO,W,wBATL,iBAQS,CART,eAQS,Q,wBAPP,iBAAgD,CAAhD,EACA,EAEA,eAGI,UAFF,eAA4E,GAA9DZ,GAAI,wBAAuB,C,wBAAE,iBAAmB,C,cAC9D,eAAqF,GAAvEA,GAAI,uBAAsB,C,wBAAE,iBAA6B,C,+GAehE,iBAAgB,CAC7Ba,WAAY,CACVC,UAAA,KACAC,SAAA,KACAC,OAAA,KACAC,cAAAC,EAAA,MAEFC,MAP6B,WAQ3B,MAAuC,eAAc,sBAA7CC,EAAR,EAAQA,QAASlB,EAAjB,EAAiBA,KAAMmB,EAAvB,EAAuBA,MAAOpB,EAA9B,EAA8BA,KAE9B,MAAO,CACLmB,UACAlB,OACAmB,QACApB,W,qBC9DN,MAAMJ,EAA2B,IAAgB,EAAQ,CAAC,CAAC,SAASC,KAErD","file":"js/favorites.ff43e072.js","sourcesContent":["\n \n \n \n\n","import { render } from \"./Column.vue?vue&type=template&id=0a418101\"\nconst script = {}\n\nimport exportComponent from \"/Users/adam/graphacademy/neoflix-ui/node_modules/vue-loader-v16/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render]])\n\nexport default __exports__","\n \n \n \n You must be logged in to save your favorite movies.\n\n \n Sign in or\n Register to continue.\n \n \n \n\n \n \n \n\n \n \n Your favorite movies will be listed here.\n Click the icon in the top right hand corner of a Movie to save it to your favorites.\n\n \n View Popular Movies or\n check out the latest releases.\n \n \n \n \n\n\n\n","import { render } from \"./Favorites.vue?vue&type=template&id=74c821fe\"\nimport script from \"./Favorites.vue?vue&type=script&lang=js\"\nexport * from \"./Favorites.vue?vue&type=script&lang=js\"\n\nimport exportComponent from \"/Users/adam/graphacademy/neoflix-ui/node_modules/vue-loader-v16/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render]])\n\nexport default __exports__"],"sourceRoot":""} -------------------------------------------------------------------------------- /src/main/resources/fixtures/roles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "role": "Don Michael Corleone", 4 | "plot": "In the midst of trying to legitimize his business dealings in New York and Italy in 1979, aging Mafia don Michael Corleone seeks to avow for his sins while taking a young protégé under his wing.", 5 | "released": "1990-12-25", 6 | "url": "https://themoviedb.org/movie/242", 7 | "year": 1990, 8 | "imdbVotes": 256626, 9 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/1hdm3Axw9LjITbApvAXBbqO58zE.jpg", 10 | "countries": [ 11 | "USA" 12 | ], 13 | "languages": [ 14 | "English", 15 | " Italian", 16 | " German", 17 | " Latin" 18 | ], 19 | "revenue": 136766062, 20 | "budget": 54000000, 21 | "tmdbId": "242", 22 | "imdbRating": 7.6, 23 | "title": "Godfather: Part III, The", 24 | "movieId": "2023", 25 | "imdbId": "0099674", 26 | "id": "0099674", 27 | "runtime": 162 28 | }, 29 | { 30 | "role": "Shylock", 31 | "plot": "In 16th century Venice, when a merchant must default on a large loan from an abused Jewish moneylender for a friend with romantic ambitions, the bitterly vengeful creditor demands a gruesome payment instead.", 32 | "released": "2005-02-18", 33 | "url": "https://themoviedb.org/movie/11162", 34 | "year": 2004, 35 | "imdbVotes": 29063, 36 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/sbYUDtqbYtqbqTvmjGBZAeWTinb.jpg", 37 | "countries": [ 38 | "USA", 39 | " Italy", 40 | " Luxembourg", 41 | " UK" 42 | ], 43 | "languages": [ 44 | "English" 45 | ], 46 | "tmdbId": "11162", 47 | "imdbRating": 7.1, 48 | "title": "Merchant of Venice, The", 49 | "movieId": "30850", 50 | "imdbId": "0379889", 51 | "id": "0379889", 52 | "runtime": 131 53 | }, 54 | { 55 | "role": "Jack Gramm", 56 | "plot": "On the day that a serial killer that he helped put away is supposed to be executed, a noted forensic psychologist and college professor receives a call informing him that he has 88 minutes left to live.", 57 | "released": "2008-04-18", 58 | "url": "https://themoviedb.org/movie/3489", 59 | "year": 2007, 60 | "imdbVotes": 64471, 61 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/8rMiBz8kLMNmQyMbQXL9MPIlyw.jpg", 62 | "countries": [ 63 | "USA", 64 | " Germany", 65 | " Canada" 66 | ], 67 | "languages": [ 68 | "English" 69 | ], 70 | "revenue": 16930884, 71 | "budget": 30000000, 72 | "tmdbId": "3489", 73 | "imdbRating": 5.9, 74 | "title": "88 Minutes", 75 | "movieId": "53207", 76 | "imdbId": "0411061", 77 | "id": "0411061", 78 | "runtime": 108 79 | }, 80 | { 81 | "role": "Walter Abrams", 82 | "plot": "After suffering a career-ending injury, a former college football star aligns himself with one of the most renowned touts in the sports-gambling business.", 83 | "released": "2005-10-07", 84 | "url": "https://themoviedb.org/movie/9910", 85 | "year": 2005, 86 | "imdbVotes": 35966, 87 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/5SedPYdGLrp6LX9C2cWXLx38w1D.jpg", 88 | "countries": [ 89 | "USA" 90 | ], 91 | "languages": [ 92 | "English" 93 | ], 94 | "revenue": 30526509, 95 | "budget": 35000000, 96 | "tmdbId": "9910", 97 | "imdbRating": 6.2, 98 | "title": "Two for the Money", 99 | "movieId": "38992", 100 | "imdbId": "0417217", 101 | "id": "0417217", 102 | "runtime": 122 103 | }, 104 | { 105 | "role": "Will Dormer", 106 | "plot": "Two Los Angeles homicide detectives are dispatched to a northern town where the sun doesn't set to investigate the methodical murder of a local teen.", 107 | "released": "2002-05-24", 108 | "url": "https://themoviedb.org/movie/320", 109 | "year": 2002, 110 | "imdbVotes": 212725, 111 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/cwB0t4OHX1Pw1Umzc9jPgzalUpS.jpg", 112 | "countries": [ 113 | "USA", 114 | " Canada" 115 | ], 116 | "languages": [ 117 | "English" 118 | ], 119 | "revenue": 113714830, 120 | "budget": 46000000, 121 | "tmdbId": "320", 122 | "imdbRating": 7.2, 123 | "title": "Insomnia", 124 | "movieId": "5388", 125 | "imdbId": "0278504", 126 | "id": "0278504", 127 | "runtime": 118 128 | } 129 | ] -------------------------------------------------------------------------------- /src/main/resources/fixtures/pulpfiction.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": [ 3 | "English", 4 | " Spanish", 5 | " French" 6 | ], 7 | "year": 1994, 8 | "imdbId": "0110912", 9 | "directors": [ 10 | { 11 | "bornIn": "Knoxville, Tennessee, USA", 12 | "tmdbId": "138", 13 | "imdbId": "0000233", 14 | "born": "1963-03-27", 15 | "name": "Quentin Tarantino", 16 | "bio": "Quentin Jerome Tarantino (born March 27, 1963) is an American film director, screenwriter, producer, cinematographer and actor. In the early 1990s he was an independent filmmaker whose films used nonlinear storylines and aestheticization of violence. His films have earned him a variety of Academy Award, Golden Globe, BAFTA and Palme d'Or Awards and he has been nominated for Emmy and Grammy Awards. In 2007, Total Film named him the 12th-greatest director of all time...", 17 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/1gjcpAa99FAOWGnrUvHEXXsRs7o.jpg", 18 | "url": "https://themoviedb.org/person/138" 19 | } 20 | ], 21 | "runtime": 154, 22 | "imdbRating": 8.9, 23 | "movieId": "296", 24 | "countries": [ 25 | "USA" 26 | ], 27 | "ratingCount": 324, 28 | "imdbVotes": 1268850, 29 | "title": "Pulp Fiction", 30 | "url": "https://themoviedb.org/movie/680", 31 | "actors": [ 32 | { 33 | "bornIn": "London, England, UK", 34 | "role": "Pumpkin", 35 | "tmdbId": "3129", 36 | "imdbId": "0000619", 37 | "born": "1961-05-14", 38 | "name": "Tim Roth", 39 | "bio": "An English film actor. Notable credits include Reservoir Dogs, Pulp Fiction, Four Rooms, Planet of the Apes, The Incredible Hulk and Rob Roy. \n\nTimothy Simon Roth is an English actor and director. He made his debut in the television film Made in Britain. He gained critical acclaim for his role as Myron in The Hit, for which he was nominated for the BAFTA Award for Most Promising Newcomer...", 40 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/qSizF2i9gz6c6DbAC5RoIq8sVqX.jpg", 41 | "url": "https://themoviedb.org/person/3129" 42 | }, 43 | { 44 | "bornIn": "Englewood, New Jersey, USA", 45 | "role": "Vincent Vega", 46 | "tmdbId": "8891", 47 | "imdbId": "0000237", 48 | "born": "1954-02-18", 49 | "name": "John Travolta", 50 | "bio": "An American actor, film producer, dancer, and singer. He first became known in the 1970s, after appearing on the television series Welcome Back, Kotter and starring in the box office successes Saturday Night Fever and Grease. Travolta's career re-surged in the 1990s, with his role in Pulp Fiction, and he has since continued starring in Hollywood films, including Face/Off, Ladder 49 and Wild Hogs. Travolta has twice been nominated for the Academy Award for Best Actor...", 51 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/JSt3skdZpGPJYJixCZqH599WdI.jpg", 52 | "url": "https://themoviedb.org/person/8891" 53 | }, 54 | { 55 | "role": "Waitress", 56 | "tmdbId": "11807", 57 | "imdbId": "0522503", 58 | "name": "Laura Lovelace", 59 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/1MLB30laQt2k80i15kdn0X7Zn5U.jpg", 60 | "url": "https://themoviedb.org/person/11807" 61 | }, 62 | { 63 | "bornIn": "Washington, D.C., USA", 64 | "role": "Jules Winnfield", 65 | "tmdbId": "2231", 66 | "imdbId": "0000168", 67 | "born": "1948-12-21", 68 | "name": "Samuel L. Jackson", 69 | "bio": "An American film and television actor and film producer. After Jackson became involved with the Civil Rights Movement, he moved on to acting in theater at Morehouse College, and then films. He had several small roles such as in the film Goodfellas, Def by Temptation, before meeting his mentor, Morgan Freeman, and the director Spike Lee. After gaining critical acclaim for his role in Jungle Fever in 1991, he appeared in films such as Patriot Games, Amos & Andrew, True Romance and Jurassic Park...", 70 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/mXN4Gw9tZJVKrLJHde2IcUHmV3P.jpg", 71 | "url": "https://themoviedb.org/person/2231" 72 | } 73 | ], 74 | "revenue": 214179088, 75 | "tmdbId": "680", 76 | "plot": "The lives of two mob hit men, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", 77 | "genres": [ 78 | { 79 | "name": "Drama" 80 | }, 81 | { 82 | "name": "Crime" 83 | }, 84 | { 85 | "name": "Comedy" 86 | }, 87 | { 88 | "name": "Thriller" 89 | } 90 | ], 91 | "favorite": false, 92 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/yAaf4ybTENKPicqzsAoW6Emxrag.jpg", 93 | "released": "1994-10-14", 94 | "budget": 8000000 95 | } -------------------------------------------------------------------------------- /src/main/resources/public/js/login.fb3010e6.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["login"],{5258:function(t,e,n){},"8d8c":function(t,e,n){"use strict";n("e272")},a55b:function(t,e,n){"use strict";n.r(e);var r=n("7a23"),o={class:"sign__group"},c={class:"sign__group"},i={class:"sign__text"},s=Object(r["i"])("Don't have an account? "),a=Object(r["i"])("Register now");function u(t,e,n,u,b,l){var d=Object(r["C"])("router-link"),g=Object(r["C"])("form-wrapper");return Object(r["u"])(),Object(r["d"])(g,{error:t.error,details:t.details,buttonText:"Sign In",onSubmit:t.onSubmit},{footer:Object(r["K"])((function(){return[Object(r["g"])("span",i,[s,Object(r["j"])(d,{to:"/register"},{default:Object(r["K"])((function(){return[a]})),_:1})])]})),default:Object(r["K"])((function(){return[Object(r["g"])("div",o,[Object(r["L"])(Object(r["g"])("input",{"onUpdate:modelValue":e[0]||(e[0]=function(e){return t.email=e}),type:"text",class:"sign__input",placeholder:"Email"},null,512),[[r["I"],t.email]])]),Object(r["g"])("div",c,[Object(r["L"])(Object(r["g"])("input",{"onUpdate:modelValue":e[1]||(e[1]=function(e){return t.password=e}),type:"password",class:"sign__input",placeholder:"Password"},null,512),[[r["I"],t.password]])])]})),_:1},8,["error","details","onSubmit"])}var b=n("5530"),l=n("d617"),d=n("6c02"),g=n("d3a2"),j=Object(r["k"])({setup:function(){var t=Object(l["a"])(),e=t.user,n=t.error,o=t.login,c=Object(d["d"])(),i=c.push,s=Object(r["y"])({email:"",password:""}),a=function(){o(s.email,s.password)};return Object(r["J"])([e],(function(){e.value&&i({name:"Home"})})),Object(b["a"])({user:e,error:n,onSubmit:a},Object(r["F"])(s))},components:{FormWrapper:g["a"]}}),O=(n("8d8c"),n("6b0d")),p=n.n(O);const f=p()(j,[["render",u],["__scopeId","data-v-109c238d"]]);e["default"]=f},d3a2:function(t,e,n){"use strict";var r=n("7a23"),o={class:"sign section--full-bg"},c={class:"container"},i={class:"row"},s={class:"col-12"},a={class:"sign__content"},u=Object(r["g"])("svg",{width:"38px",height:"36px",viewBox:"0 0 38 36",version:"1.1",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink"},[Object(r["g"])("g",{id:"Page-1",stroke:"none","stroke-width":"1",fill:"none","fill-rule":"evenodd"},[Object(r["g"])("g",{id:"Artboard-Copy-4",transform:"translate(-461.000000, -92.000000)",fill:"#FFFFFF","fill-rule":"nonzero"},[Object(r["g"])("g",{id:"Neo4j-logo-white",transform:"translate(461.000000, 92.000000)"},[Object(r["g"])("path",{d:"M15.1567882,19.7787499 C14.2593113,19.7797412 13.3869911,20.0839625 12.6739274,20.6446466 L8.12902953,17.4480446 C8.23017901,17.0744431 8.28207734,16.6885531 8.28333162,16.3007314 C8.30323173,14.5779313 7.31020034,13.0132192 5.76906756,12.3390243 C4.22793477,11.6648295 2.44364988,12.0145572 1.25142245,13.2245027 C0.0591950158,14.4344482 -0.305064777,16.2651954 0.329150182,17.859786 C0.963365141,19.4543767 2.47056054,20.4972837 4.14523019,20.5003305 C5.04296547,20.5009503 5.91577446,20.1965585 6.62809105,19.6344338 L11.1589614,22.8526832 C10.9531573,23.6053568 10.9531573,24.4018518 11.1589614,25.1545253 L6.61406359,28.3511274 C5.9055597,27.7920045 5.03816115,27.4877809 4.14523019,27.4852306 C2.47025128,27.4823124 0.958755122,28.5185053 0.316457436,30.1100122 C-0.325840249,31.7015192 0.0277521654,33.5344193 1.21214281,34.7529338 C2.39653346,35.9714483 4.17810422,36.3352281 5.72504187,35.674425 C7.27197952,35.0136219 8.27915444,33.4585776 8.27631789,31.7353403 C8.27610073,31.3526759 8.22657313,30.97173 8.12902953,30.6024588 L12.6739274,27.4058568 C13.3869911,27.9665408 14.2593113,28.2707622 15.1567882,28.2717535 C17.3222528,28.1125873 19,26.2587846 19,24.0252517 C19,21.7917188 17.3222528,19.9379161 15.1567882,19.7787499 L15.1567882,19.7787499 Z",id:"Path"}),Object(r["g"])("path",{d:"M25.5070592,0 C18.0226312,0 13,4.36522213 13,12.8334696 L13,18.871082 C13.7598872,18.496936 14.5938987,18.2983633 15.4405743,18.2899973 C16.2888621,18.2901903 17.1259465,18.4840715 17.8882228,18.8569092 L17.8882228,12.805124 C17.8882228,7.31316435 20.9159498,4.49277732 25.5282816,4.49277732 C30.1406134,4.49277732 33.1258956,7.31316435 33.1258956,12.805124 L33.1258956,26 L38,26 L38,12.805124 C38.0141184,4.28727174 32.9914872,0 25.5070592,0 Z",id:"Path"})])])])],-1),b={key:0,class:"sign__group error-message"},l=["innerHTML"];function d(t,e,n,d,g,j){return Object(r["u"])(),Object(r["f"])("div",o,[Object(r["g"])("div",c,[Object(r["g"])("div",i,[Object(r["g"])("div",s,[Object(r["g"])("div",a,[Object(r["g"])("form",{action:"#",class:"sign__form",onSubmit:e[0]||(e[0]=Object(r["M"])((function(){return t.onSubmit&&t.onSubmit.apply(t,arguments)}),["prevent"]))},[u,t.error?(Object(r["u"])(),Object(r["f"])("div",b,Object(r["E"])(t.error),1)):Object(r["e"])("",!0),Object(r["B"])(t.$slots,"default"),Object(r["g"])("button",{class:"sign__btn",type:"submit",innerHTML:t.buttonText},null,8,l),Object(r["B"])(t.$slots,"footer")],32)])])])])])}var g=Object(r["k"])({props:{error:String,details:Object,buttonText:String,onSubmit:Function}}),j=(n("f24f"),n("6b0d")),O=n.n(j);const p=O()(g,[["render",d]]);e["a"]=p},e272:function(t,e,n){},f24f:function(t,e,n){"use strict";n("5258")}}]); 2 | //# sourceMappingURL=login.fb3010e6.js.map -------------------------------------------------------------------------------- /src/main/java/neoflix/services/FavoriteService.java: -------------------------------------------------------------------------------- 1 | package neoflix.services; 2 | 3 | import neoflix.AppUtils; 4 | import neoflix.Params; 5 | import org.neo4j.driver.Driver; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public class FavoriteService { 13 | private final Driver driver; 14 | 15 | private final List> popular; 16 | private final List> users; 17 | private final Map>> userFavorites = new HashMap<>(); 18 | 19 | /** 20 | * The constructor expects an instance of the Neo4j Driver, which will be 21 | * used to interact with Neo4j. 22 | * 23 | * @param driver 24 | */ 25 | public FavoriteService(Driver driver) { 26 | this.driver = driver; 27 | this.popular = AppUtils.loadFixtureList("popular"); 28 | this.users = AppUtils.loadFixtureList("users"); 29 | } 30 | 31 | /** 32 | * This method should retrieve a list of movies that have an incoming :HAS_FAVORITE 33 | * relationship from a User node with the supplied `userId`. 34 | * 35 | * Results should be ordered by the `sort` parameter, and in the direction specified 36 | * in the `order` parameter. 37 | * Results should be limited to the number passed as `limit`. 38 | * The `skip` variable should be used to skip a certain number of rows. 39 | * 40 | * @param userId The unique ID of the user 41 | * @param params Query params for pagination and sorting 42 | * @return List An list of Movie objects 43 | */ 44 | // tag::all[] 45 | public List> all(String userId, Params params) { 46 | // TODO: Open a new session 47 | // TODO: Retrieve a list of movies favorited by the user 48 | // TODO: Close session 49 | 50 | return AppUtils.process(userFavorites.getOrDefault(userId, List.of()),params); 51 | } 52 | // end::all[] 53 | 54 | /** 55 | * This method should create a `:HAS_FAVORITE` relationship between 56 | * the User and Movie ID nodes provided. 57 | * 58 | * If either the user or movie cannot be found, a `NotFoundError` should be thrown. 59 | * 60 | * @param userId The unique ID for the User node 61 | * @param movieId The unique tmdbId for the Movie node 62 | * @return Map The updated movie record with `favorite` set to true 63 | */ 64 | // tag::add[] 65 | public Map add(String userId, String movieId) { 66 | // TODO: Open a new Session 67 | // TODO: Create HAS_FAVORITE relationship within a Write Transaction 68 | // TODO: Close the session 69 | // TODO: Return movie details and `favorite` property 70 | 71 | var foundMovie = popular.stream().filter(m -> movieId.equals(m.get("tmdbId"))).findAny(); 72 | 73 | if (users.stream().anyMatch(u -> u.get("userId").equals(userId)) || foundMovie.isEmpty()) { 74 | throw new RuntimeException("Couldn't create a favorite relationship for User %s and Movie %s".formatted(userId, movieId)); 75 | } 76 | 77 | var movie = foundMovie.get(); 78 | var favorites = userFavorites.computeIfAbsent(userId, (k) -> new ArrayList<>()); 79 | if (!favorites.contains(movie)) { 80 | favorites.add(movie); 81 | } 82 | var copy = new HashMap<>(movie); 83 | copy.put("favorite", true); 84 | return copy; 85 | } 86 | // end::add[] 87 | 88 | /* 89 | *This method should remove the `:HAS_FAVORITE` relationship between 90 | * the User and Movie ID nodes provided. 91 | * If either the user, movie or the relationship between them cannot be found, 92 | * a `NotFoundError` should be thrown. 93 | 94 | * @param userId The unique ID for the User node 95 | * @param movieId The unique tmdbId for the Movie node 96 | * @return Map The updated movie record with `favorite` set to true 97 | */ 98 | // tag::remove[] 99 | public Map remove(String userId, String movieId) { 100 | // TODO: Open a new Session 101 | // TODO: Delete the HAS_FAVORITE relationship within a Write Transaction 102 | // TODO: Close the session 103 | // TODO: Return movie details and `favorite` property 104 | if (users.stream().anyMatch(u -> u.get("userId").equals(userId))) { 105 | throw new RuntimeException("Couldn't remove a favorite relationship for User %s and Movie %s".formatted(userId, movieId)); 106 | } 107 | 108 | var movie = popular.stream().filter(m -> movieId.equals(m.get("tmdbId"))).findAny().get(); 109 | var favorites = userFavorites.computeIfAbsent(userId, (k) -> new ArrayList<>()); 110 | if (favorites.contains(movie)) { 111 | favorites.remove(movie); 112 | } 113 | 114 | var copy = new HashMap<>(movie); 115 | copy.put("favorite", false); 116 | return copy; 117 | } 118 | // end::remove[] 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/neoflix/services/AuthService.java: -------------------------------------------------------------------------------- 1 | package neoflix.services; 2 | 3 | import neoflix.AppUtils; 4 | import neoflix.AuthUtils; 5 | import neoflix.ValidationException; 6 | import org.neo4j.driver.Driver; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class AuthService { 14 | 15 | 16 | private final Driver driver; 17 | private final List> users; 18 | private String jwtSecret; 19 | 20 | /** 21 | * The constructor expects an instance of the Neo4j Driver, which will be 22 | * used to interact with Neo4j. 23 | * 24 | * @param driver 25 | * @param jwtSecret 26 | */ 27 | public AuthService(Driver driver, String jwtSecret) { 28 | this.driver = driver; 29 | this.jwtSecret = jwtSecret; 30 | this.users = AppUtils.loadFixtureList("users"); 31 | } 32 | 33 | /** 34 | * This method should create a new User node in the database with the email and name 35 | * provided, along with an encrypted version of the password and a `userId` property 36 | * generated by the server. 37 | * 38 | * The properties also be used to generate a JWT `token` which should be included 39 | * with the returned user. 40 | * 41 | * @param email 42 | * @param plainPassword 43 | * @param name 44 | * @return User 45 | */ 46 | // tag::register[] 47 | public Map register(String email, String plainPassword, String name) { 48 | var encrypted = AuthUtils.encryptPassword(plainPassword); 49 | // tag::constraintError[] 50 | // TODO: Handle Unique constraints in the database 51 | var foundUser = users.stream().filter(u -> u.get("email").equals(email)).findAny(); 52 | if (foundUser.isPresent()) { 53 | throw new RuntimeException("An account already exists with the email address"); 54 | } 55 | // end::constraintError[] 56 | 57 | // TODO: Save user in database 58 | var user = Map.of("email",email, "name",name, 59 | "userId", String.valueOf(email.hashCode()), "password", encrypted); 60 | users.add(user); 61 | 62 | String sub = (String) user.get("userId"); 63 | String token = AuthUtils.sign(sub,userToClaims(user), jwtSecret); 64 | 65 | return userWithToken(user, token); 66 | } 67 | // end::register[] 68 | 69 | 70 | /** 71 | * This method should attempt to find a user by the email address provided 72 | * and attempt to verify the password. 73 | * 74 | * If a user is not found or the passwords do not match, a `false` value should 75 | * be returned. Otherwise, the users properties should be returned along with 76 | * an encoded JWT token with a set of 'claims'. 77 | * 78 | * { 79 | * userId: 'some-random-uuid', 80 | * email: 'graphacademy@neo4j.com', 81 | * name: 'GraphAcademy User', 82 | * token: '...' 83 | * } 84 | * 85 | * @param email The user's email address 86 | * @param plainPassword An attempt at the user's password in unencrypted form 87 | * @return User Resolves to a null value when the user is not found or password is incorrect. 88 | */ 89 | // tag::authenticate[] 90 | public Map authenticate(String email, String plainPassword) { 91 | // TODO: Authenticate the user from the database 92 | var foundUser = users.stream().filter(u -> u.get("email").equals(email)).findAny(); 93 | if (foundUser.isEmpty()) 94 | throw new ValidationException("Incorrect email", Map.of("email","Incorrect email")); 95 | var user = foundUser.get(); 96 | if (!plainPassword.equals(user.get("password")) && 97 | !AuthUtils.verifyPassword(plainPassword,(String)user.get("password"))) { // 98 | throw new ValidationException("Incorrect password", Map.of("password","Incorrect password")); 99 | } 100 | // tag::return[] 101 | String sub = (String) user.get("userId"); 102 | String token = AuthUtils.sign(sub, userToClaims(user), jwtSecret); 103 | return userWithToken(user, token); 104 | // end::return[] 105 | } 106 | // end::authenticate[] 107 | 108 | private Map userToClaims(Map user) { 109 | return Map.of( 110 | "sub", user.get("userId"), 111 | "userId", user.get("userId"), 112 | "name", user.get("name") 113 | ); 114 | } 115 | private Map claimsToUser(Map claims) { 116 | return Map.of( 117 | "userId", claims.get("sub"), 118 | "name", claims.get("name") 119 | ); 120 | } 121 | private Map userWithToken(Map user, String token) { 122 | return Map.of( 123 | "token", token, 124 | "userId", user.get("userId"), 125 | "email", user.get("email"), 126 | "name", user.get("name") 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/neoflix/_11_MovieListTest.java: -------------------------------------------------------------------------------- 1 | package neoflix; 2 | 3 | import neoflix.services.MovieService; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.neo4j.driver.Driver; 8 | import org.neo4j.driver.Values; 9 | 10 | import static neoflix.Params.Sort.released; 11 | import static neoflix.Params.Sort.title; 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | class _11_MovieListTest { 15 | private static Driver driver; 16 | 17 | private static final String userId = "fe770c6b-4034-4e07-8e40-2f39e7a6722c"; 18 | private static final String email = "graphacademy.movielists@neo4j.com"; 19 | private static final String tomHanks = "31"; 20 | private static final String coppola = "1776"; 21 | 22 | @BeforeAll 23 | static void initDriver() { 24 | AppUtils.loadProperties(); 25 | driver = AppUtils.initDriver(); 26 | 27 | if (driver != null) driver.session().executeWrite(tx -> tx.run(""" 28 | MERGE (u:User {userId: $userId}) SET u.email = $email 29 | """, Values.parameters("userId", userId, "email", email))); 30 | } 31 | 32 | @AfterAll 33 | static void closeDriver() { 34 | if (driver != null) driver.close(); 35 | } 36 | 37 | @Test 38 | void getPaginatedMoviesByGenre() { 39 | MovieService movieService = new MovieService(driver); 40 | 41 | var genreName = "Comedy"; 42 | var limit = 3; 43 | 44 | var output = movieService.byGenre(genreName, new Params(null, title, Params.Order.ASC, limit, 0), userId); 45 | assertNotNull(output); 46 | assertEquals(limit, output.size()); 47 | 48 | var secondOutput = movieService.byGenre(genreName, new Params(null, title, Params.Order.ASC, limit, limit), userId); 49 | assertNotNull(secondOutput); 50 | assertEquals(limit, secondOutput.size()); 51 | 52 | assertNotEquals(output.get(0).get("title"), secondOutput.get(0).get("title")); 53 | 54 | var reordered = movieService.byGenre(genreName, new Params(null, released, Params.Order.ASC, limit, limit), userId); 55 | assertNotEquals(output.get(0).get("title"), reordered.get(0).get("title")); 56 | } 57 | 58 | @Test 59 | void getPaginatedMoviesByActor() { 60 | MovieService movieService = new MovieService(driver); 61 | 62 | var limit = 2; 63 | 64 | var output = movieService.getForActor(tomHanks, new Params(null, title, Params.Order.ASC, limit, 0), userId); 65 | assertNotNull(output); 66 | assertEquals(limit, output.size()); 67 | assertEquals("'burbs, The", output.get(0).get("title")); 68 | 69 | var secondOutput = movieService.getForActor(tomHanks, new Params(null, title, Params.Order.ASC, limit, limit), userId); 70 | assertNotNull(secondOutput); 71 | assertEquals(limit, secondOutput.size()); 72 | assertEquals("Apollo 13", secondOutput.get(0).get("title")); 73 | 74 | assertNotEquals(output.get(0).get("title"), secondOutput.get(0).get("title")); 75 | 76 | var reordered = movieService.getForActor(tomHanks, new Params(null, released, Params.Order.ASC, limit, limit), userId); 77 | assertNotEquals(output.get(0).get("title"), reordered.get(0).get("title")); 78 | } 79 | 80 | @Test 81 | void getPaginatedMoviesByDirector() { 82 | MovieService movieService = new MovieService(driver); 83 | 84 | var limit = 1; 85 | 86 | var output = movieService.getForDirector(coppola, new Params(null, title, Params.Order.ASC, limit, 0), userId); 87 | assertNotNull(output); 88 | assertEquals(limit, output.size()); 89 | assertEquals("Apocalypse Now", output.get(0).get("title")); 90 | 91 | var secondOutput = movieService.getForDirector(coppola, new Params(null, title, Params.Order.ASC, limit, limit), userId); 92 | assertNotNull(secondOutput); 93 | assertEquals(limit, secondOutput.size()); 94 | assertEquals("Conversation, The", secondOutput.get(0).get("title")); 95 | 96 | assertNotEquals(output.get(0).get("title"), secondOutput.get(0).get("title")); 97 | 98 | var reordered = movieService.getForDirector(coppola, new Params(null, title, Params.Order.DESC, limit, 0), userId); 99 | assertNotEquals(output.get(0).get("title"), reordered.get(0).get("title")); 100 | } 101 | 102 | @Test 103 | void getMoviesDirectedByCoppola() { 104 | MovieService movieService = new MovieService(driver); 105 | 106 | var output = movieService.getForDirector(coppola, new Params(null, title, Params.Order.ASC, 30, 0), userId); 107 | assertNotNull(output); 108 | assertEquals(16, output.size()); 109 | assertEquals("Apocalypse Now", output.get(0).get("title")); 110 | assertEquals("Tucker: The Man and His Dream", output.get(15).get("title")); 111 | 112 | System.out.println(""" 113 | 114 | Here is the answer to the quiz question on the lesson: 115 | How many films has Francis Ford Coppola directed? 116 | Copy and paste the following answer into the text box: 117 | """); 118 | System.out.println(output.size()); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/resources/public/js/register.f0868032.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["register"],{5258:function(t,e,n){},"73cf":function(t,e,n){"use strict";n.r(e);n("b0c0");var r=n("7a23"),o={class:"sign__group"},i={class:"sign__group"},s={class:"sign__group"},c={class:"sign__text"},a=Object(r["i"])("Already have an account? "),l=Object(r["i"])("Sign In");function u(t,e,n,u,b,d){var j=Object(r["C"])("router-link"),g=Object(r["C"])("form-wrapper");return Object(r["u"])(),Object(r["d"])(g,{error:t.error,details:t.details,buttonText:"Register",onSubmit:t.onSubmit},{footer:Object(r["K"])((function(){return[Object(r["g"])("span",c,[a,Object(r["j"])(j,{to:"/login"},{default:Object(r["K"])((function(){return[l]})),_:1})])]})),default:Object(r["K"])((function(){return[Object(r["g"])("div",o,[Object(r["L"])(Object(r["g"])("input",{"onUpdate:modelValue":e[0]||(e[0]=function(e){return t.name=e}),type:"text",class:Object(r["p"])(["sign__input",{error:t.details&&t.details.name}]),placeholder:"Your Name"},null,2),[[r["I"],t.name]])]),Object(r["g"])("div",i,[Object(r["L"])(Object(r["g"])("input",{"onUpdate:modelValue":e[1]||(e[1]=function(e){return t.email=e}),type:"text",class:Object(r["p"])(["sign__input",{error:t.details&&t.details.email}]),placeholder:"Email"},null,2),[[r["I"],t.email]])]),Object(r["g"])("div",s,[Object(r["L"])(Object(r["g"])("input",{"onUpdate:modelValue":e[2]||(e[2]=function(e){return t.password=e}),type:"password",class:Object(r["p"])(["sign__input",{error:t.details&&t.details.password}]),placeholder:"Password"},null,2),[[r["I"],t.password]])])]})),_:1},8,["error","details","onSubmit"])}var b=n("5530"),d=n("d617"),j=n("6c02"),g=n("d3a2"),p=Object(r["k"])({setup:function(){var t=Object(d["a"])(),e=t.user,n=t.error,o=t.details,i=t.register,s=Object(j["d"])(),c=s.push,a=Object(r["y"])({email:"",password:"",name:""}),l=function(){i(a.email,a.password,a.name)};return Object(r["J"])([e],(function(){e.value&&c({name:"Home"})})),Object(b["a"])({user:e,error:n,details:o,onSubmit:l},Object(r["F"])(a))},components:{FormWrapper:g["a"]}}),O=n("6b0d"),f=n.n(O);const m=f()(p,[["render",u]]);e["default"]=m},d3a2:function(t,e,n){"use strict";var r=n("7a23"),o={class:"sign section--full-bg"},i={class:"container"},s={class:"row"},c={class:"col-12"},a={class:"sign__content"},l=Object(r["g"])("svg",{width:"38px",height:"36px",viewBox:"0 0 38 36",version:"1.1",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink"},[Object(r["g"])("g",{id:"Page-1",stroke:"none","stroke-width":"1",fill:"none","fill-rule":"evenodd"},[Object(r["g"])("g",{id:"Artboard-Copy-4",transform:"translate(-461.000000, -92.000000)",fill:"#FFFFFF","fill-rule":"nonzero"},[Object(r["g"])("g",{id:"Neo4j-logo-white",transform:"translate(461.000000, 92.000000)"},[Object(r["g"])("path",{d:"M15.1567882,19.7787499 C14.2593113,19.7797412 13.3869911,20.0839625 12.6739274,20.6446466 L8.12902953,17.4480446 C8.23017901,17.0744431 8.28207734,16.6885531 8.28333162,16.3007314 C8.30323173,14.5779313 7.31020034,13.0132192 5.76906756,12.3390243 C4.22793477,11.6648295 2.44364988,12.0145572 1.25142245,13.2245027 C0.0591950158,14.4344482 -0.305064777,16.2651954 0.329150182,17.859786 C0.963365141,19.4543767 2.47056054,20.4972837 4.14523019,20.5003305 C5.04296547,20.5009503 5.91577446,20.1965585 6.62809105,19.6344338 L11.1589614,22.8526832 C10.9531573,23.6053568 10.9531573,24.4018518 11.1589614,25.1545253 L6.61406359,28.3511274 C5.9055597,27.7920045 5.03816115,27.4877809 4.14523019,27.4852306 C2.47025128,27.4823124 0.958755122,28.5185053 0.316457436,30.1100122 C-0.325840249,31.7015192 0.0277521654,33.5344193 1.21214281,34.7529338 C2.39653346,35.9714483 4.17810422,36.3352281 5.72504187,35.674425 C7.27197952,35.0136219 8.27915444,33.4585776 8.27631789,31.7353403 C8.27610073,31.3526759 8.22657313,30.97173 8.12902953,30.6024588 L12.6739274,27.4058568 C13.3869911,27.9665408 14.2593113,28.2707622 15.1567882,28.2717535 C17.3222528,28.1125873 19,26.2587846 19,24.0252517 C19,21.7917188 17.3222528,19.9379161 15.1567882,19.7787499 L15.1567882,19.7787499 Z",id:"Path"}),Object(r["g"])("path",{d:"M25.5070592,0 C18.0226312,0 13,4.36522213 13,12.8334696 L13,18.871082 C13.7598872,18.496936 14.5938987,18.2983633 15.4405743,18.2899973 C16.2888621,18.2901903 17.1259465,18.4840715 17.8882228,18.8569092 L17.8882228,12.805124 C17.8882228,7.31316435 20.9159498,4.49277732 25.5282816,4.49277732 C30.1406134,4.49277732 33.1258956,7.31316435 33.1258956,12.805124 L33.1258956,26 L38,26 L38,12.805124 C38.0141184,4.28727174 32.9914872,0 25.5070592,0 Z",id:"Path"})])])])],-1),u={key:0,class:"sign__group error-message"},b=["innerHTML"];function d(t,e,n,d,j,g){return Object(r["u"])(),Object(r["f"])("div",o,[Object(r["g"])("div",i,[Object(r["g"])("div",s,[Object(r["g"])("div",c,[Object(r["g"])("div",a,[Object(r["g"])("form",{action:"#",class:"sign__form",onSubmit:e[0]||(e[0]=Object(r["M"])((function(){return t.onSubmit&&t.onSubmit.apply(t,arguments)}),["prevent"]))},[l,t.error?(Object(r["u"])(),Object(r["f"])("div",u,Object(r["E"])(t.error),1)):Object(r["e"])("",!0),Object(r["B"])(t.$slots,"default"),Object(r["g"])("button",{class:"sign__btn",type:"submit",innerHTML:t.buttonText},null,8,b),Object(r["B"])(t.$slots,"footer")],32)])])])])])}var j=Object(r["k"])({props:{error:String,details:Object,buttonText:String,onSubmit:Function}}),g=(n("f24f"),n("6b0d")),p=n.n(g);const O=p()(j,[["render",d]]);e["a"]=O},f24f:function(t,e,n){"use strict";n("5258")}}]); 2 | //# sourceMappingURL=register.f0868032.js.map -------------------------------------------------------------------------------- /diff/13-listing-ratings.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/MovieService.java b/src/main/java/neoflix/services/MovieService.java 2 | index c6e7b7a..0938190 100644 3 | --- a/src/main/java/neoflix/services/MovieService.java 4 | +++ b/src/main/java/neoflix/services/MovieService.java 5 | @@ -98,32 +98,10 @@ public class MovieService { 6 | */ 7 | // tag::findById[] 8 | public Map findById(String id, String userId) { 9 | - // Find a movie by its ID 10 | + // TODO: Find a movie by its ID 11 | // MATCH (m:Movie {tmdbId: $id}) 12 | 13 | - // Open a new database session 14 | - try (var session = this.driver.session()) { 15 | - // Find a movie by its ID 16 | - return session.executeRead(tx -> { 17 | - var favorites = getUserFavorites(tx, userId); 18 | - 19 | - String query = """ 20 | - MATCH (m:Movie {tmdbId: $id}) 21 | - RETURN m { 22 | - .*, 23 | - actors: [ (a)-[r:ACTED_IN]->(m) | a { .*, role: r.role } ], 24 | - directors: [ (d)-[:DIRECTED]->(m) | d { .* } ], 25 | - genres: [ (m)-[:IN_GENRE]->(g) | g { .name }], 26 | - ratingCount: size((m)<-[:RATED]-()), 27 | - favorite: m.tmdbId IN $favorites 28 | - } AS movie 29 | - LIMIT 1 30 | - """; 31 | - var res = tx.run(query, Values.parameters("id", id, "favorites", favorites)); 32 | - return res.single().get("movie").asMap(); 33 | - }); 34 | - 35 | - } 36 | + return popular.stream().filter(m -> id.equals(m.get("tmdbId"))).findAny().get(); 37 | } 38 | // end::findById[] 39 | 40 | @@ -147,39 +125,14 @@ public class MovieService { 41 | */ 42 | // tag::getSimilarMovies[] 43 | public List> getSimilarMovies(String id, Params params, String userId) { 44 | - // Get similar movies based on genres or ratings 45 | - // MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m) 46 | - 47 | - // Open an Session 48 | - try (var session = this.driver.session()) { 49 | - 50 | - // Get similar movies based on genres or ratings 51 | - var movies = session.executeRead(tx -> { 52 | - var favorites = getUserFavorites(tx, userId); 53 | - String query = """ 54 | - MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m) 55 | - WHERE m.imdbRating IS NOT NULL 56 | - 57 | - WITH m, count(*) AS inCommon 58 | - WITH m, inCommon, m.imdbRating * inCommon AS score 59 | - ORDER BY score DESC 60 | - 61 | - SKIP $skip 62 | - LIMIT $limit 63 | - 64 | - RETURN m { 65 | - .*, 66 | - score: score, 67 | - favorite: m.tmdbId IN $favorites 68 | - } AS movie 69 | - """; 70 | - var res = tx.run(query, Values.parameters("id", id, "skip", params.skip(), "limit", params.limit(), "favorites", favorites)); 71 | - // Get a list of Movies from the Result 72 | - return res.list(row -> row.get("movie").asMap()); 73 | - }); 74 | - 75 | - return movies; 76 | - } 77 | + // TODO: Get similar movies based on genres or ratings 78 | + 79 | + return AppUtils.process(popular, params).stream() 80 | + .map(item -> { 81 | + Map copy = new HashMap<>(item); 82 | + copy.put("score", ((int)(Math.random() * 10000)) / 100.0); 83 | + return copy; 84 | + }).toList(); 85 | } 86 | // end::getSimilarMovies[] 87 | 88 | diff --git a/src/main/java/neoflix/services/RatingService.java b/src/main/java/neoflix/services/RatingService.java 89 | index 1b04ee1..a2a1e09 100644 90 | --- a/src/main/java/neoflix/services/RatingService.java 91 | +++ b/src/main/java/neoflix/services/RatingService.java 92 | @@ -44,9 +44,25 @@ public class RatingService { 93 | */ 94 | // tag::forMovie[] 95 | public List> forMovie(String id, Params params) { 96 | - // TODO: Get ratings for a Movie 97 | + // Open a new database session 98 | + try (var session = this.driver.session()) { 99 | 100 | - return AppUtils.process(ratings,params); 101 | + // Get ratings for a Movie 102 | + return session.executeRead(tx -> { 103 | + String query = String.format(""" 104 | + MATCH (u:User)-[r:RATED]->(m:Movie {tmdbId: $id}) 105 | + RETURN r { 106 | + .rating, 107 | + .timestamp, 108 | + user: u { .id, .name } 109 | + } AS review 110 | + ORDER BY r.`%s` %s 111 | + SKIP $skip 112 | + LIMIT $limit""", params.sort(Params.Sort.timestamp), params.order()); 113 | + var res = tx.run(query, Values.parameters("id", id, "limit", params.limit(), "skip", params.skip())); 114 | + return res.list(row -> row.get("review").asMap()); 115 | + }); 116 | + } 117 | } 118 | // end::forMovie[] 119 | 120 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/people.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bornIn": "Toronto, Ontario, Canada", 4 | "tmdbId": "129097", 5 | "imdbId": "0005616", 6 | "name": " Aaron Woodley", 7 | "url": "https://themoviedb.org/person/129097" 8 | }, 9 | { 10 | "bornIn": "Orimattila, Finland", 11 | "tmdbId": "16767", 12 | "imdbId": "0442454", 13 | "born": "1957-04-04", 14 | "name": " Aki Kaurismäki", 15 | "bio": "Aki Kaurismäki is a Finnish film writer, director, actor and producer. Together with his older brother, filmmaker Mika Kaurismäki, he founded the production and distribution company Villealfa. He's a graduate in Media Studies from the University of Tampere, Finland...", 16 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/kiJErWEOv4Ew7aHOGKg4ljsmppZ.jpg", 17 | "url": "https://themoviedb.org/person/16767" 18 | }, 19 | { 20 | "tmdbId": "57406", 21 | "imdbId": "0073688", 22 | "name": " Alec Berg", 23 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/8qCkqxlyxrSybMnUWf85XQ11qMB.jpg", 24 | "url": "https://themoviedb.org/person/57406" 25 | }, 26 | { 27 | "bornIn": "Mexico City, Distrito Federal, Mexico", 28 | "tmdbId": "223", 29 | "imdbId": "0327944", 30 | "born": "1963-08-15", 31 | "name": " Alejandro González Iñárritu", 32 | "bio": "Alejandro González Iñárritu (American Spanish: [aleˈxandɾo gonˈsales iˈɲaritu]; credited since 2014 as Alejandro G. Iñárritu; born August 15, 1963) is a Mexican film director, producer and screenwriter. He is the first Mexican director to be nominated for the Academy Award for Best Director and the Directors Guild of America Award for Outstanding Directing, for Babel (2007)...", 33 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/qWrltG9e0ssM3Y9pF86EAgteKHu.jpg", 34 | "url": "https://themoviedb.org/person/223" 35 | }, 36 | { 37 | "tmdbId": "1292356", 38 | "imdbId": "1780501", 39 | "name": " Alejo Crisóstomo", 40 | "url": "https://themoviedb.org/person/1292356" 41 | }, 42 | { 43 | "bornIn": "Boston, Massachusetts, USA", 44 | "tmdbId": "3111", 45 | "imdbId": "0734319", 46 | "born": "1956-08-18", 47 | "name": " Alexandre Rockwell", 48 | "bio": "Alexandre Rockwell was born in Boston in 1956. His first film, LENZ, premiered at the Berlin Film Festival in 1982. After his road movie HERO (1983), he directed the legendary SAM FULLER IN SONS (1989). In 1992 he won the Grand Jury Prize at the Sundance Film Festival for his film IN THE SOUP, which made his reputation as one of the most interesting filmmakers on America’s independent film scene. SOMEBODY TO LOVE (1994) competed at the Venice flim Festival...", 49 | "url": "https://themoviedb.org/person/3111" 50 | }, 51 | { 52 | "bornIn": "Detroit, Michigan, USA", 53 | "tmdbId": "1776", 54 | "imdbId": "0000338", 55 | "born": "1939-04-07", 56 | "name": "Francis Ford Coppola", 57 | "bio": "Francis Ford Coppola (born April 7, 1939) is an American film director, producer and screenwriter. He is widely acclaimed as one of Hollywood's most celebrated and influential film directors. He epitomized the group of filmmakers known as the New Hollywood, which included George Lucas, Martin Scorsese, Robert Altman, Woody Allen and William Friedkin, who emerged in the early 1970s with unconventional ideas that challenged contemporary filmmaking...", 58 | "directedCount": 16, 59 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/mGKkVp3l9cipPt10AqoQnwaPrfI.jpg", 60 | "url": "https://themoviedb.org/person/1776", 61 | "actedCount": 2 62 | }, 63 | { 64 | "bornIn": "New York City, New York, USA", 65 | "tmdbId": "1158", 66 | "id": "0000199", 67 | "born": "1940-04-25", 68 | "name": "Al Pacino", 69 | "bio": "Alfredo James \"Al\" Pacino (born April 25, 1940) is an American film and stage actor and director. He is famous for playing mobsters, including Michael Corleone in The Godfather trilogy, Tony Montana in Scarface, Alphonse \"Big Boy\" Caprice in Dick Tracy and Carlito Brigante in Carlito's Way, though he has also appeared several times on the other side of the law — as a police officer, detective and a lawyer...", 70 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/sLsw9Dtj4mkL8aPmCrh38Ap9Xhq.jpg", 71 | "url": "https://themoviedb.org/person/1158", 72 | "directedCount": 2, 73 | "actedCount": 3 74 | }, 75 | { 76 | "bornIn": "Seattle, Washington, U.S.", 77 | "tmdbId": "19858", 78 | "imdbId": "1494624", 79 | "born": "1991-09-21", 80 | "name": "Zoe Weizenbaum", 81 | "bio": "From Wikipedia, the free encyclopedia. \n\nZoë Weizenbaum (born September 21, 1991) is an American child actress. \n\nWeizenbaum was born in Seattle, Washington to a Jewish American mother and a Chinese father and was raised in the Jewish religion. She grew up from the age of two in Amherst, Massachusetts. For years, African Dance was her primary love. She also enjoyed being part of Amherst's local musical theatre productions...", 82 | "url": "https://themoviedb.org/person/19858" 83 | }, 84 | { 85 | "tmdbId": "1581097", 86 | "imdbId": "5746225", 87 | "name": "Zoey Vargas", 88 | "url": "https://themoviedb.org/person/1581097" 89 | }, 90 | { 91 | "bornIn": "Erandio, Vizcaya, Spain", 92 | "tmdbId": "3813", 93 | "imdbId": "0029962", 94 | "born": "12-Apr.-1953", 95 | "name": "Álex Angulo", 96 | "bio": "Alejandro “Álex” Angulo León was a Spanish Basque actor who performed in over sixty films during his career spanning more than 30 years. Angulo died at the age of 61 when the vehicle in which he was travelling veered from the road...", 97 | "died": "20-Juli-2014", 98 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/xzVsmWbKhTcIGo0YhcMj4BLwhGX.jpg", 99 | "url": "https://themoviedb.org/person/3813" 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /src/main/resources/fixtures/comedy_movies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "languages": [ 4 | "English" 5 | ], 6 | "year": 2004, 7 | "imdbId": "0424755", 8 | "runtime": 143, 9 | "imdbRating": 6.9, 10 | "movieId": "97757", 11 | "countries": [ 12 | "USA" 13 | ], 14 | "imdbVotes": 126, 15 | "title": "'Hellboy': The Seeds of Creation", 16 | "url": "https://themoviedb.org/movie/72867", 17 | "tmdbId": "72867", 18 | "plot": "In-depth documentary about the history of the Hellboy comic book, the history of the character and the making of the 2004 movie adaptation of the same name.", 19 | "favorite": false, 20 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/358FTzyn2TusjvdqoW0lLMr7KTY.jpg", 21 | "released": "2004-07-27" 22 | }, 23 | { 24 | "languages": [ 25 | "English" 26 | ], 27 | "year": 1989, 28 | "imdbId": "0096734", 29 | "runtime": 101, 30 | "imdbRating": 6.8, 31 | "movieId": "2072", 32 | "countries": [ 33 | "USA" 34 | ], 35 | "imdbVotes": 49593, 36 | "title": "'burbs, The", 37 | "url": "https://themoviedb.org/movie/11974", 38 | "revenue": 36602000, 39 | "tmdbId": "11974", 40 | "plot": "An overstressed suburbanite and two of his neighbors struggle to prove their paranoid theory that the new family on the block are part of a murderous cult.", 41 | "favorite": false, 42 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/1oyRR8D5WiVHwunIkOMjl30Jdg5.jpg", 43 | "released": "1989-02-17", 44 | "budget": 18000000 45 | }, 46 | { 47 | "languages": [ 48 | "English", 49 | " French", 50 | " Swedish" 51 | ], 52 | "year": 2009, 53 | "imdbId": "1022603", 54 | "runtime": 95, 55 | "imdbRating": 7.8, 56 | "movieId": "69757", 57 | "countries": [ 58 | "USA" 59 | ], 60 | "imdbVotes": 363490, 61 | "title": "(500) Days of Summer", 62 | "url": "https://themoviedb.org/movie/19913", 63 | "revenue": 60722734, 64 | "tmdbId": "19913", 65 | "plot": "An offbeat romantic comedy about a woman who doesn't believe true love exists, and the young man who falls for her.", 66 | "favorite": false, 67 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/f9mbM0YMLpYemcWx6o2WeiYQLDP.jpg", 68 | "released": "2009-08-07", 69 | "budget": 7500000 70 | }, 71 | { 72 | "languages": [ 73 | "English" 74 | ], 75 | "year": 1987, 76 | "imdbId": "0092494", 77 | "runtime": 106, 78 | "imdbRating": 6.5, 79 | "movieId": "8169", 80 | "countries": [ 81 | "USA" 82 | ], 83 | "imdbVotes": 22418, 84 | "title": "*batteries not included", 85 | "url": "https://themoviedb.org/movie/11548", 86 | "revenue": 65088797, 87 | "tmdbId": "11548", 88 | "plot": "Apartment block tenants seek the aid of alien mechanical life-forms to save their building from demolition.", 89 | "favorite": false, 90 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/8NkpnpdLd8J6zCR8ZGTeGTur4uO.jpg", 91 | "released": "1987-12-18" 92 | }, 93 | { 94 | "languages": [ 95 | "English" 96 | ], 97 | "year": 1993, 98 | "imdbId": "0107492", 99 | "runtime": 82, 100 | "imdbRating": 6.7, 101 | "movieId": "6600", 102 | "countries": [ 103 | "USA" 104 | ], 105 | "imdbVotes": 758, 106 | "title": "...And God Spoke", 107 | "url": "https://themoviedb.org/movie/41660", 108 | "tmdbId": "41660", 109 | "plot": "A documentary on the making of a big budget Bible picture. This is a spoof that shows the inside action on a film set where everything that could possibly go wrong goes wrong.", 110 | "favorite": false, 111 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/wLuy2URSbEr4iRbJHGUlJcKItgv.jpg", 112 | "released": "1994-09-23" 113 | }, 114 | { 115 | "languages": [ 116 | "English" 117 | ], 118 | "year": 1979, 119 | "imdbId": "0078721", 120 | "runtime": 122, 121 | "imdbRating": 6, 122 | "movieId": "6658", 123 | "countries": [ 124 | "USA" 125 | ], 126 | "imdbVotes": 11920, 127 | "title": "10", 128 | "url": "https://themoviedb.org/movie/9051", 129 | "revenue": 74865517, 130 | "tmdbId": "9051", 131 | "plot": "A Hollywood lyricist goes through a mid-life crisis and becomes infatuated with a sexy, newly married woman.", 132 | "favorite": false, 133 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/kC8cInADdO6eUe94d8rv3GTT0Ng.jpg", 134 | "released": "1979-10-05" 135 | }, 136 | { 137 | "languages": [ 138 | "English" 139 | ], 140 | "year": 1917, 141 | "imdbId": "0008133", 142 | "runtime": 30, 143 | "imdbRating": 7.8, 144 | "movieId": "8511", 145 | "countries": [ 146 | "USA" 147 | ], 148 | "imdbVotes": 4947, 149 | "title": "Immigrant, The", 150 | "url": "https://themoviedb.org/movie/47653", 151 | "tmdbId": "47653", 152 | "plot": "Charlie is an immigrant who endures a challenging voyage and gets into trouble as soon as he arrives in America.", 153 | "favorite": false, 154 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/uCRkOWSGahdQPBIVtecnUUoM4U0.jpg", 155 | "released": "1917-06-17" 156 | }, 157 | { 158 | "languages": [ 159 | "English" 160 | ], 161 | "year": 1918, 162 | "imdbId": "0009018", 163 | "runtime": 33, 164 | "imdbRating": 7.8, 165 | "movieId": "3309", 166 | "countries": [ 167 | "USA" 168 | ], 169 | "imdbVotes": 4582, 170 | "title": "Dog's Life, A", 171 | "url": "https://themoviedb.org/movie/36208", 172 | "tmdbId": "36208", 173 | "plot": "The Little Tramp and his dog companion struggle to survive in the inner city.", 174 | "favorite": false, 175 | "poster": "https://image.tmdb.org/t/p/w440_and_h660_face/41vqtliesQrsJQ9iTJh5nFYQgBg.jpg", 176 | "released": "1918-04-14" 177 | }, 178 | { 179 | "languages": [ 180 | "English" 181 | ], 182 | "year": 1919, 183 | "imdbId": "0009932", 184 | "runtime": 12, 185 | "imdbRating": 6.1, 186 | "movieId": "72626", 187 | "countries": [ 188 | "USA" 189 | ], 190 | "imdbVotes": 503, 191 | "title": "Billy Blazes, Esq.", 192 | "url": "https://themoviedb.org/movie/53516", 193 | "tmdbId": "53516", 194 | "plot": "Billy Blazes confronts Crooked Charley, who has been ruling the town of Peaceful Vale through fear and violence.", 195 | "favorite": false, 196 | "released": "1919-07-06" 197 | } 198 | ] -------------------------------------------------------------------------------- /diff/11-movie-lists.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/main/java/neoflix/services/MovieService.java b/src/main/java/neoflix/services/MovieService.java 2 | index fdf7591..0938190 100644 3 | --- a/src/main/java/neoflix/services/MovieService.java 4 | +++ b/src/main/java/neoflix/services/MovieService.java 5 | @@ -51,7 +51,7 @@ public class MovieService { 6 | // Execute a query in a new Read Transaction 7 | var movies = session.executeRead(tx -> { 8 | // Get an array of IDs for the User's favorite movies 9 | - var favorites = getUserFavorites(tx, userId); 10 | + var favorites = getUserFavorites(tx, userId); 11 | 12 | // Retrieve a list of movies with the 13 | // favorite flag appened to the movie's properties 14 | @@ -156,10 +156,36 @@ public class MovieService { 15 | */ 16 | // tag::getByGenre[] 17 | public List> byGenre(String name, Params params, String userId) { 18 | - // TODO: Get Movies in a Genre 19 | + // Get Movies in a Genre 20 | // MATCH (m:Movie)-[:IN_GENRE]->(:Genre {name: $name}) 21 | 22 | - return AppUtils.process(comedyMovies, params); 23 | + // Open a new session and close at the end 24 | + try (var session = driver.session()) { 25 | + // Execute a query in a new Read Transaction 26 | + return session.executeRead((tx) -> { 27 | + // Get an array of IDs for the User's favorite movies 28 | + var favorites = getUserFavorites(tx, userId); 29 | + 30 | + // Retrieve a list of movies with the 31 | + // favorite flag append to the movie's properties 32 | + var result = tx.run( 33 | + String.format(""" 34 | + MATCH (m:Movie)-[:IN_GENRE]->(:Genre {name: $name}) 35 | + WHERE m.`%s` IS NOT NULL 36 | + RETURN m { 37 | + .*, 38 | + favorite: m.tmdbId IN $favorites 39 | + } AS movie 40 | + ORDER BY m.`%s` %s 41 | + SKIP $skip 42 | + LIMIT $limit 43 | + """, params.sort(), params.sort(), params.order()), 44 | + Values.parameters("skip", params.skip(), "limit", params.limit(), 45 | + "favorites", favorites, "name", name)); 46 | + var movies = result.list(row -> row.get("movie").asMap()); 47 | + return movies; 48 | + }); 49 | + } 50 | } 51 | // end::getByGenre[] 52 | 53 | @@ -182,10 +208,37 @@ public class MovieService { 54 | */ 55 | // tag::getForActor[] 56 | public List> getForActor(String actorId, Params params,String userId) { 57 | - // TODO: Get Movies acted in by a Person 58 | + // Get Movies acted in by a Person 59 | // MATCH (:Person {tmdbId: $id})-[:ACTED_IN]->(m:Movie) 60 | 61 | - return AppUtils.process(actedInTomHanks, params); 62 | + // Open a new session 63 | + try (var session = this.driver.session()) { 64 | + 65 | + // Execute a query in a new Read Transaction 66 | + var movies = session.executeRead(tx -> { 67 | + // Get an array of IDs for the User's favorite movies 68 | + var favorites = getUserFavorites(tx, userId); 69 | + var sort = params.sort(Params.Sort.title); 70 | + 71 | + // Retrieve a list of movies with the 72 | + // favorite flag appended to the movie's properties 73 | + String query = String.format(""" 74 | + MATCH (:Person {tmdbId: $id})-[:ACTED_IN]->(m:Movie) 75 | + WHERE m.`%s` IS NOT NULL 76 | + RETURN m { 77 | + .*, 78 | + favorite: m.tmdbId IN $favorites 79 | + } AS movie 80 | + ORDER BY m.`%s` %s 81 | + SKIP $skip 82 | + LIMIT $limit 83 | + """, sort, sort, params.order()); 84 | + var res = tx.run(query, Values.parameters("skip", params.skip(), "limit", params.limit(), "favorites", favorites, "id", actorId)); 85 | + // Get a list of Movies from the Result 86 | + return res.list(row -> row.get("movie").asMap()); 87 | + }); 88 | + return movies; 89 | + } 90 | } 91 | // end::getForActor[] 92 | 93 | @@ -208,10 +261,37 @@ public class MovieService { 94 | */ 95 | // tag::getForDirector[] 96 | public List> getForDirector(String directorId, Params params,String userId) { 97 | - // TODO: Get Movies directed by a Person 98 | + // Get Movies acted in by a Person 99 | // MATCH (:Person {tmdbId: $id})-[:DIRECTED]->(m:Movie) 100 | 101 | - return AppUtils.process(directedByCoppola, params); 102 | + // Open a new session 103 | + try (var session = this.driver.session()) { 104 | + 105 | + // Execute a query in a new Read Transaction 106 | + var movies = session.executeRead(tx -> { 107 | + // Get an array of IDs for the User's favorite movies 108 | + var favorites = getUserFavorites(tx, userId); 109 | + var sort = params.sort(Params.Sort.title); 110 | + 111 | + // Retrieve a list of movies with the 112 | + // favorite flag appended to the movie's properties 113 | + String query = String.format(""" 114 | + MATCH (:Person {tmdbId: $id})-[:DIRECTED]->(m:Movie) 115 | + WHERE m.`%s` IS NOT NULL 116 | + RETURN m { 117 | + .*, 118 | + favorite: m.tmdbId IN $favorites 119 | + } AS movie 120 | + ORDER BY m.`%s` %s 121 | + SKIP $skip 122 | + LIMIT $limit 123 | + """, sort, sort, params.order()); 124 | + var res = tx.run(query, Values.parameters("skip", params.skip(), "limit", params.limit(), "favorites", favorites, "id", directorId)); 125 | + // Get a list of Movies from the Result 126 | + return res.list(row -> row.get("movie").asMap()); 127 | + }); 128 | + return movies; 129 | + } 130 | } 131 | // end::getForDirector[] 132 | 133 | @@ -236,6 +316,4 @@ public class MovieService { 134 | return favoriteResult.list(row -> row.get("id").asString()); 135 | } 136 | // end::getUserFavorites[] 137 | - 138 | - record Movie() {} // todo 139 | -} 140 | +} 141 | \ No newline at end of file 142 | -------------------------------------------------------------------------------- /src/main/java/example/Index.java: -------------------------------------------------------------------------------- 1 | package example; 2 | // tag::import[] 3 | // Import all relevant classes from neo4j-java-driver dependency 4 | import org.neo4j.driver.*; 5 | // end::import[] 6 | 7 | import java.util.*; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.logging.Level; 10 | 11 | public class Index { 12 | /** 13 | * Example Authentication token. 14 | * 15 | * You can use `AuthTokens.basic` to create a token. A basic token accepts a 16 | * Username and Password 17 | */ 18 | // tag::credentials[] 19 | static String username = "neo4j"; 20 | static String password = "letmein!"; 21 | // end::credentials[] 22 | 23 | // tag::auth[] 24 | AuthToken authenticationToken = AuthTokens.basic(username, password); 25 | // end::auth[] 26 | 27 | /* 28 | * Here is the pseudocode for creating the Driver: 29 | 30 | // tag::pseudo[] 31 | var driver = GraphDatabase.driver( 32 | connectionString, // <1> 33 | authenticationToken, // <2> 34 | configuration // <3> 35 | ) 36 | // end::pseudo[] 37 | 38 | The first argument is the connection string, it is constructed like so: 39 | 40 | // tag::connection[] 41 | address of server 42 | ↓ 43 | neo4j://localhost:7687 44 | ↑ ↑ 45 | scheme port number 46 | // end::connection[] 47 | */ 48 | 49 | /** 50 | * The following code creates an instance of the Neo4j Driver 51 | */ 52 | // tag::driver[] 53 | // Create a new Driver instance 54 | static Driver driver = GraphDatabase.driver("neo4j://localhost:7687", 55 | AuthTokens.basic(username, password)); 56 | // end::driver[] 57 | 58 | // tag::configuration[] 59 | Config config = Config.builder() 60 | .withConnectionTimeout(30, TimeUnit.SECONDS) 61 | .withMaxConnectionLifetime(30, TimeUnit.MINUTES) 62 | .withMaxConnectionPoolSize(10) 63 | .withConnectionAcquisitionTimeout(20, TimeUnit.SECONDS) 64 | .withFetchSize(1000) 65 | .withDriverMetrics() 66 | .withLogging(Logging.console(Level.INFO)) 67 | .build(); 68 | // end::configuration[] 69 | 70 | /** 71 | * It is considered best practise to inject an instance of the driver. 72 | * This way the object can be mocked within unit tests 73 | */ 74 | public static class MyService { 75 | private final Driver driver; 76 | 77 | public MyService(Driver driver) { 78 | this.driver = driver; 79 | } 80 | 81 | public void method() { 82 | // tag::session[] 83 | // Open a new session 84 | try (var session = driver.session()) { 85 | 86 | // Do something with the session... 87 | 88 | // Close the session automatically in try-with-resources block 89 | } 90 | // end::session[] 91 | } 92 | } 93 | 94 | /** 95 | * These functions are wrapped in an `async` function so that we can use the await 96 | * keyword rather than the Promise API. 97 | */ 98 | public static void main () { 99 | // tag::verifyConnectivity[] 100 | // Verify the connection details 101 | driver.verifyConnectivity(); 102 | // end::verifyConnectivity[] 103 | 104 | System.out.println("Connection verified!"); 105 | 106 | // tag::driver.session[] 107 | // Open a new session 108 | var session = driver.session(); 109 | // end::driver.session[] 110 | 111 | // tag::session.run[] 112 | var query = "MATCH () RETURN count(*) AS count"; 113 | var params = Values.parameters(); 114 | 115 | // Run a query in an auto-commit transaction 116 | var res = session.run(query, params).single().get("count").asLong(); 117 | // end::session.run[] 118 | 119 | System.out.println(res); 120 | 121 | // tag::session.close[] 122 | // Close the session 123 | session.close(); 124 | // end::session.close[] 125 | 126 | new MyService(driver).method(); 127 | 128 | driver.close(); 129 | } 130 | private static void showReadTransaction (Driver driver){ 131 | try (var session = driver.session()) { 132 | 133 | // tag::session.readTransaction[] 134 | // Run a query within a Read Transaction 135 | var res = session.readTransaction(tx -> { 136 | return tx.run(""" 137 | MATCH (p:Person)-[:ACTED_IN]->(m:Movie) 138 | WHERE m.title = $title // <1> 139 | RETURN p.name AS name 140 | LIMIT 10 141 | """, 142 | Values.parameters("title", "Arthur") // <2> 143 | ).list(r -> r.get("name").asString()); 144 | // end::session.readTransaction[] 145 | }); 146 | 147 | } 148 | } 149 | 150 | private static void showWriteTransaction (Driver driver){ 151 | try (var session = driver.session()) { 152 | 153 | // tag::session.writeTransaction[] 154 | session.writeTransaction(tx -> { 155 | return tx.run( 156 | "CREATE (p:Person {name: $name})", 157 | Values.parameters("name", "Michael")).consume(); 158 | }); 159 | // end::session.writeTransaction[] 160 | } 161 | } 162 | 163 | private static void showManualTransaction(Driver driver){ 164 | // tag::session.beginTransaction[] 165 | // Open a new session 166 | var session = driver.session( 167 | SessionConfig.builder() 168 | .withDefaultAccessMode(AccessMode.WRITE) 169 | .build()); 170 | 171 | // Manually create a transaction 172 | var tx = session.beginTransaction(); 173 | // end::session.beginTransaction[] 174 | 175 | var query = "MATCH (n) RETURN count(n) AS count"; 176 | var params = Values.parameters(); 177 | 178 | // tag::session.beginTransaction.Try[] 179 | try { 180 | // Perform an action 181 | tx.run(query, params); 182 | 183 | // Commit the transaction 184 | tx.commit(); 185 | } catch (Exception e) { 186 | // If something went wrong, rollback the transaction 187 | tx.rollback(); 188 | } 189 | // end::session.beginTransaction.Try[] 190 | 191 | session.close(); 192 | } 193 | 194 | /** 195 | * This is an example function that will create a new Person node within 196 | * a read transaction and return the properties for the node. 197 | * 198 | * @param {string} name 199 | * @return {Record} The properties for the node 200 | */ 201 | // tag::createPerson[] 202 | private static Map createPerson(String name) { 203 | // tag::sessionWithArgs[] 204 | // Create a Session for the `people` database 205 | var sessionConfig = SessionConfig.builder() 206 | .withDefaultAccessMode(AccessMode.WRITE) 207 | .withDatabase("people") 208 | .build(); 209 | 210 | try (var session = driver.session(sessionConfig)) { 211 | // end::sessionWithArgs[] 212 | 213 | // Create a node within a write transaction 214 | var res = session.writeTransaction(tx -> 215 | tx.run("CREATE (p:Person {name: $name}) RETURN p", 216 | Values.parameters("name", name)) 217 | .single()); 218 | 219 | // Get the `p` value from the first record 220 | var p = res.get("p").asNode(); 221 | 222 | // Return the properties of the node 223 | return p.asMap(); 224 | // Autoclose the sesssion 225 | } 226 | } 227 | // end::createPerson[] 228 | 229 | // Run the main method above 230 | // main() 231 | } 232 | --------------------------------------------------------------------------------
You must be logged in to save your favorite movies.
\n Sign in or\n Register to continue.\n
Your favorite movies will be listed here.
Click the icon in the top right hand corner of a Movie to save it to your favorites.
\n View Popular Movies or\n check out the latest releases.\n