├── .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 | 
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 | 
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 | Movie |
48 | Released |
49 | Tagline |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
Details
60 |
61 |
62 |
![]()
63 |
64 |
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 |
--------------------------------------------------------------------------------