├── .babelrc ├── README.md ├── img ├── graphiql.png └── screen_shot.png ├── lib └── index.html ├── package.json ├── src ├── index.js ├── public │ └── js │ │ ├── handleMovieSearch.js │ │ ├── models │ │ ├── Movie.js │ │ └── MovieCast.js │ │ └── movie-client.js ├── resolvers.js └── typeDefs.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ['env'] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neo4j Movies Example Application - GraphQL JavaScript Edition 2 | 3 | ![](./img/screen_shot.png) 4 | 5 | ## Quickstart 6 | 7 | * Start Neo4j ([Download & Install](http://neo4j.com/download)) locally and open the [Neo4j Browser](http://localhost:7474). 8 | * Install the Movies dataset with `:play movies`, click the statement, and hit the triangular "Run" button. 9 | 10 | Optionally, set environment variables to override defaults: 11 | 12 | PORT = 3000, 13 | NEO4J_URL = "bolt://localhost:7687", 14 | NEO4J_USER = "neo4j", 15 | NEO4J_PASSWORD = "letmein" 16 | 17 | Then 18 | 19 | ``` 20 | npm install 21 | npm run start 22 | ``` 23 | 24 | Then navigate to `http://localhost:3000` in your browser. 25 | 26 | ## How It Works 27 | 28 | The frontend for this example application uses jQuery, D3, and ApolloClient to query a GraphQL API. 29 | 30 | The backend is an Express.js GraphQL service using graphql-tools and the JavaScript Neo4j driver. 31 | 32 | JavaScript assets are transpiled and bundled using Webpack. 33 | 34 | ## Query with GraphiQL 35 | 36 | ![](./img/graphiql.png) 37 | 38 | The GraphiQL tool for querying the GraphQL service is available at `http://localhost:3000/graphiql` 39 | 40 | -------------------------------------------------------------------------------- /img/graphiql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-examples/movies-graphql-javascript/9a5493e72b7e92195fcf35707610b16066528555/img/graphiql.png -------------------------------------------------------------------------------- /img/screen_shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-examples/movies-graphql-javascript/9a5493e72b7e92195fcf35707610b16066528555/img/screen_shot.png -------------------------------------------------------------------------------- /lib/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Neo4j Movies 6 | 7 | 8 | 9 |
10 |
11 | 39 | 40 |
41 |
42 |
43 |
Search Results
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
MovieReleasedTagline
55 |
56 |
57 |
58 |
59 |
Details
60 |
61 |
62 | 63 |
64 |
65 |

Crew

66 |
    67 |
68 |
69 |
70 |
71 |
72 |
73 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movies-graphql-javascript", 3 | "version": "0.0.1", 4 | "description": "Neo4j example movie search application with GraphQL backend", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "./node_modules/.bin/webpack && babel-node ./src/index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/neo4j-examples/movies-graphql-javascript.git" 13 | }, 14 | "keywords": [ 15 | "Neo4j", 16 | "GraphQL", 17 | "JavaScript" 18 | ], 19 | "author": "William Lyon", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/neo4j-examples/movies-graphql-javascript/issues" 23 | }, 24 | "homepage": "https://github.com/neo4j-examples/movies-graphql-javascript#readme", 25 | "dependencies": { 26 | "apollo-client-preset": "^1.0.8", 27 | "apollo-server-express": "^1.3.2", 28 | "babel-cli": "^6.26.0", 29 | "babel-preset-env": "^1.6.1", 30 | "cors": "^2.8.4", 31 | "express": "^4.16.3", 32 | "graphql": "^0.13.2", 33 | "graphql-tools": "^2.22.0", 34 | "lodash": "^4.17.5", 35 | "neo4j-driver": "^1.5.3", 36 | "webpack": "^4.1.1", 37 | "webpack-cli": "^2.0.12" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'; 4 | import { makeExecutableSchema } from 'graphql-tools'; 5 | import typeDefs from './typeDefs'; 6 | import resolvers from './resolvers'; 7 | import { cors } from 'cors'; 8 | import { v1 as neo4j } from 'neo4j-driver'; 9 | 10 | const { 11 | PORT = 3000, 12 | NEO4J_URL = "bolt://localhost:7687", 13 | NEO4J_USER = "neo4j", 14 | NEO4J_PASSWORD = "letmein" 15 | } = process.env; 16 | 17 | 18 | const app = express(); 19 | 20 | app.use(express.static('lib')); 21 | app.use(bodyParser.json()); 22 | 23 | const schema = makeExecutableSchema({ typeDefs, resolvers }); 24 | 25 | app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`)); 26 | 27 | app.use( 28 | '/graphql', 29 | graphqlExpress(req => ({ 30 | schema, 31 | context: { 32 | driver: neo4j.driver(NEO4J_URL, neo4j.auth.basic(NEO4J_USER, NEO4J_PASSWORD)) 33 | } 34 | })) 35 | ); 36 | 37 | app.use( 38 | '/graphiql', 39 | graphiqlExpress({ 40 | endpointURL: '/graphql' 41 | }) 42 | ) -------------------------------------------------------------------------------- /src/public/js/handleMovieSearch.js: -------------------------------------------------------------------------------- 1 | var api = require("./movie-client"); 2 | 3 | $(function () { 4 | renderGraph(); 5 | search(); 6 | 7 | $("#search").submit(e => { 8 | e.preventDefault(); 9 | search(); 10 | }); 11 | }); 12 | 13 | function showMovie(title) { 14 | api 15 | .getMovie(title) 16 | .then(movie => { 17 | if (!movie) return; 18 | 19 | $("#title").text(movie.title); 20 | $("#poster").attr("src", "http://neo4j-contrib.github.io/developer-resources/language-guides/assets/posters/" + movie.title + ".jpg"); 21 | var $list = $("#crew").empty(); 22 | movie.cast.forEach(cast => { 23 | $list.append($("
  • " + cast.name + " " + cast.job + (cast.job == "acted" ? " as " + cast.role : "") + "
  • ")); 24 | }); 25 | }, "json"); 26 | } 27 | 28 | function search() { 29 | var query = $("#search").find("input[name=search]").val(); 30 | api 31 | .searchMovies(query) 32 | .then(movies => { 33 | var t = $("table#results tbody").empty(); 34 | 35 | if (movies) { 36 | movies.forEach(movie => { 37 | $("" + movie.title + "" + movie.released + "" + movie.tagline + "").appendTo(t) 38 | .click(function() { 39 | showMovie($(this).find("td.movie").text()); 40 | }) 41 | }); 42 | 43 | var first = movies[0]; 44 | if (first) { 45 | showMovie(first.title); 46 | } 47 | } 48 | }); 49 | } 50 | 51 | function renderGraph() { 52 | var width = 800, height = 800; 53 | var force = d3.layout.force() 54 | .charge(-200).linkDistance(30).size([width, height]); 55 | 56 | var svg = d3.select("#graph").append("svg") 57 | .attr("width", "100%").attr("height", "100%") 58 | .attr("pointer-events", "all"); 59 | 60 | api 61 | .getGraph() 62 | .then(graph => { 63 | force.nodes(graph.nodes).links(graph.links).start(); 64 | 65 | var link = svg.selectAll(".link") 66 | .data(graph.links).enter() 67 | .append("line").attr("class", "link"); 68 | 69 | var node = svg.selectAll(".node") 70 | .data(graph.nodes).enter() 71 | .append("circle") 72 | .attr("class", d => { 73 | return "node " + d.label 74 | }) 75 | .attr("r", 10) 76 | .call(force.drag); 77 | 78 | // html title attribute 79 | node.append("title") 80 | .text(d => { 81 | return d.title; 82 | }); 83 | 84 | // force feed algo ticks 85 | force.on("tick", () => { 86 | link.attr("x1", d => { 87 | return d.source.x; 88 | }).attr("y1", d => { 89 | return d.source.y; 90 | }).attr("x2", d => { 91 | return d.target.x; 92 | }).attr("y2", d => { 93 | return d.target.y; 94 | }); 95 | 96 | node.attr("cx", d => { 97 | return d.x; 98 | }).attr("cy", d => { 99 | return d.y; 100 | }); 101 | }); 102 | }); 103 | } -------------------------------------------------------------------------------- /src/public/js/models/Movie.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function Movie(movie) { 4 | _.extend(this, movie); 5 | 6 | if (this.id) { 7 | this.id = this.id.toNumber(); 8 | } 9 | if (this.duration) { 10 | this.duration = this.duration.toNumber(); 11 | } 12 | } 13 | 14 | module.exports = Movie; 15 | -------------------------------------------------------------------------------- /src/public/js/models/MovieCast.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function MovieCast(title, cast) { 4 | _.extend(this, { 5 | title: title, 6 | cast: cast.map(function (c) { 7 | return { 8 | name: c.actor.name, 9 | job: 'acted', 10 | role: c.roles 11 | } 12 | }) 13 | }); 14 | } 15 | 16 | module.exports = MovieCast; 17 | -------------------------------------------------------------------------------- /src/public/js/movie-client.js: -------------------------------------------------------------------------------- 1 | 2 | var Movie = require('./models/Movie'); 3 | var MovieCast = require('./models/MovieCast'); 4 | var _ = require('lodash'); 5 | 6 | var ApolloClient = require('apollo-client-preset'); 7 | var gql = require('graphql-tag'); 8 | 9 | const client = new ApolloClient.default(); 10 | 11 | function searchMovies(queryString) { 12 | 13 | return client 14 | .query({ 15 | query: gql`query movieSearch($title:String){ 16 | movieSearch(title: $title) { 17 | title 18 | tagline 19 | released 20 | } 21 | }`, 22 | variables: {title: queryString} 23 | }) 24 | .then(data => { 25 | console.log(data); 26 | return data.data.movieSearch.map(record => { 27 | return new Movie(record); 28 | }) 29 | }) 30 | .catch(error => {throw error}) 31 | } 32 | 33 | function getMovie(title) { 34 | return client 35 | .query({ 36 | query: gql`query movieSeach($title:String){ 37 | movieSearch(title: $title) { 38 | title 39 | cast { 40 | roles 41 | actor { 42 | name 43 | } 44 | } 45 | } 46 | }`, 47 | variables: {title} 48 | }) 49 | .then(data => { 50 | console.log(data); 51 | const movie = data.data.movieSearch[0]; 52 | return new MovieCast(movie.title, movie.cast); 53 | 54 | }) 55 | .catch(error => {throw error}); 56 | } 57 | 58 | function getGraph() { 59 | 60 | return client 61 | .query({ 62 | query: gql`{ 63 | movieSearch(title: "") { 64 | title 65 | cast { 66 | actor { 67 | name 68 | } 69 | } 70 | } 71 | }` 72 | }) 73 | .then(data => { 74 | var nodes = [], rels = [], i = 0; 75 | data.data.movieSearch.forEach(res => { 76 | nodes.push({title: res.title, label: 'movie'}); 77 | var target = i; 78 | i++; 79 | 80 | res.cast.forEach(c => { 81 | var actor = {title: c.actor.name, label: 'actor'}; 82 | var source = _.findIndex(nodes, actor); 83 | if (source == -1) { 84 | nodes.push(actor); 85 | source = i; 86 | i++; 87 | } 88 | rels.push({source, target}) 89 | }) 90 | }) 91 | 92 | return {nodes, links: rels}; 93 | }) 94 | .catch(error => {throw error}); 95 | } 96 | 97 | exports.searchMovies = searchMovies; 98 | exports.getMovie = getMovie; 99 | exports.getGraph = getGraph; -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | const resolvers = { 2 | Query: { 3 | movieSearch: (_, args, context) => { 4 | const session = context.driver.session(); 5 | 6 | var query = ` 7 | MATCH (movie:Movie) 8 | WHERE toLower(movie.title) CONTAINS toLower($title) 9 | RETURN movie 10 | `; 11 | 12 | return session.run(query, args) 13 | .then(result => { 14 | return result.records.map( 15 | record => { return record.get("movie").properties } 16 | ) 17 | }) 18 | } 19 | }, 20 | Movie: { 21 | cast: (obj, args, context) => { 22 | const session = context.driver.session(); 23 | 24 | var query = ` 25 | MATCH (m:Movie {title: $title})<-[r:ACTED_IN]-(actor:Person) 26 | RETURN r.roles AS roles, actor 27 | `; 28 | 29 | return session.run(query, {title: obj.title}) 30 | .then(result => { 31 | return result.records.map( 32 | (record) => { 33 | const c = {}; 34 | c['roles'] = record.get("roles"); 35 | c['actor'] = record.get("actor").properties; 36 | return c; 37 | 38 | } 39 | ) 40 | }) 41 | } 42 | } 43 | 44 | } 45 | 46 | export default resolvers; 47 | -------------------------------------------------------------------------------- /src/typeDefs.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | type Query { 3 | movieSearch(title: String): [Movie] 4 | } 5 | 6 | type Movie { 7 | title: String 8 | released: Int 9 | tagline: String 10 | cast: [ActorRole] 11 | } 12 | 13 | type ActorRole { 14 | roles: [String] 15 | actor: Actor 16 | } 17 | 18 | type Actor { 19 | name: String 20 | } 21 | 22 | `; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/public/js/handleMovieSearch.js', 5 | output: { 6 | filename: 'bundle.js', 7 | libraryTarget: 'umd', 8 | path: path.resolve(__dirname, 'lib/js') 9 | } 10 | }; 11 | --------------------------------------------------------------------------------