├── .gitignore
├── README.md
├── akka-http
└── src
│ └── main
│ ├── resources
│ ├── application.conf
│ └── index.html
│ └── scala
│ └── neotypes
│ └── exaple
│ └── akkahttp
│ ├── Boot.scala
│ ├── Config.scala
│ ├── MovieRoute.scala
│ ├── MovieService.scala
│ └── model.scala
├── build.sbt
├── example.png
└── project
├── build.properties
└── plugins.sbt
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 |
4 | # sbt specific
5 | .cache
6 | .history
7 | .lib/
8 | dist/*
9 | target/
10 | lib_managed/
11 | src_managed/
12 | project/boot/
13 | project/plugins/project/
14 |
15 | # Scala-IDE specific
16 | .idea/
17 | rest.iml
18 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Example app
2 |
3 | 
4 |
5 | This example application demonstrates how easy it is to get started with [neotypes](https://github.com/neotypes/neotypes).
6 |
7 | It is a very simple web application that uses the Movie graph dataset to provide a search with listing, a detail view and a graph visualization.
8 |
9 | The front-end is just jQuery and d3 and the backend is implemented in Scala using akka-http.
10 |
11 | ## The Stack
12 |
13 | These are the components of our min- Web Application:
14 |
15 | * Application Type: Scala-Web Application
16 | * Web framework: Akka-http
17 | * Neo4j Database Connector: neotypes
18 | * Database: Neo4j-Server
19 | * Frontend: jquery, bootstrap, d3.js
20 |
21 | ## Endpoints:
22 |
23 | Get Movie
24 |
25 | ```
26 | // JSON object for single movie with cast
27 | curl http://localhost:8080/movie/The%20Matrix
28 |
29 | // list of JSON objects for movie search results
30 | curl http://localhost:8080/search?q=matrix
31 |
32 | // JSON object for whole graph viz (nodes, links - arrays)
33 | curl http://localhost:8080/graph
34 | ```
35 |
36 | ## How to start
37 |
38 | 1. Run `Boot.scala`
39 | 2. Go to [http://localhost:9000/index.html](http://localhost:9000/index.html)
40 |
--------------------------------------------------------------------------------
/akka-http/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | http {
2 | host = "0.0.0.0"
3 | port = 9000
4 | }
5 |
6 | database = {
7 | url = "bolt://localhost:7687"
8 | url = ${?BOLT_URL}
9 | username = "neo4j"
10 | username = ${?NEO4J_USER}
11 | password = "pass"
12 | password = ${?NEO4J_PASSWORD}
13 | }
--------------------------------------------------------------------------------
/akka-http/src/main/resources/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 |
116 |
117 |
159 |
160 |
--------------------------------------------------------------------------------
/akka-http/src/main/scala/neotypes/exaple/akkahttp/Boot.scala:
--------------------------------------------------------------------------------
1 | package neotypes.exaple.akkahttp
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.stream.ActorMaterializer
6 | import neotypes.{Driver, GraphDatabase}
7 | import org.neo4j.driver.AuthTokens
8 |
9 | import scala.concurrent.{ExecutionContext, Future}
10 |
11 | object Boot extends App {
12 |
13 | type Id[A] = A
14 |
15 | def startApplication() = {
16 |
17 | implicit val actorSystem = ActorSystem()
18 | implicit val executor: ExecutionContext = actorSystem.dispatcher
19 | implicit val materializer: ActorMaterializer = ActorMaterializer()
20 |
21 | val config = Config.load()
22 |
23 | val driver: Id[Driver[Future]] = GraphDatabase.driver[Future](
24 | config.database.url,
25 | AuthTokens.basic(config.database.username, config.database.password)
26 | )
27 |
28 | val movieService = new MovieService(driver)
29 | val httpRoute = new MovieRoute(movieService)
30 |
31 | Http().bindAndHandle(httpRoute.route, config.http.host, config.http.port)
32 |
33 | Runtime.getRuntime().addShutdownHook(new Thread(() => driver.close))
34 | }
35 |
36 | startApplication()
37 | }
38 |
--------------------------------------------------------------------------------
/akka-http/src/main/scala/neotypes/exaple/akkahttp/Config.scala:
--------------------------------------------------------------------------------
1 | package neotypes.exaple.akkahttp
2 |
3 | import pureconfig.{ConfigSource}
4 | import pureconfig.generic.auto._
5 |
6 | case class Config(http: HttpConfig, database: DatabaseConfig)
7 |
8 | object Config {
9 | def load(): Config = ConfigSource.default.load[Config] match {
10 | case Right(config) => config
11 | case Left(error) =>
12 | throw new RuntimeException(
13 | "Cannot read config file, errors: $error.toList.mkString(\"\\n\")"
14 | )
15 | }
16 | }
17 |
18 | case class HttpConfig(host: String, port: Int)
19 | case class DatabaseConfig(url: String, username: String, password: String)
20 |
--------------------------------------------------------------------------------
/akka-http/src/main/scala/neotypes/exaple/akkahttp/MovieRoute.scala:
--------------------------------------------------------------------------------
1 | package neotypes.exaple.akkahttp
2 |
3 | import akka.http.scaladsl.model.StatusCodes
4 | import akka.http.scaladsl.server.Directives._
5 | import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
6 | import io.circe.generic.auto._
7 | import io.circe.syntax._
8 | import akka.http.scaladsl.server.directives._
9 | import ContentTypeResolver.Default
10 |
11 | import scala.concurrent.ExecutionContext
12 |
13 | class MovieRoute(movieService: MovieService)(implicit executionContext: ExecutionContext)
14 | extends FailFastCirceSupport {
15 |
16 | import StatusCodes._
17 |
18 | val route = pathPrefix("movie") {
19 | pathPrefix(Segment) { title =>
20 | pathEndOrSingleSlash {
21 | get {
22 | complete(movieService.findMovie(title).map {
23 | case Some(movie) =>
24 | OK -> movie.asJson
25 | case None =>
26 | BadRequest -> None.asJson
27 | })
28 | }
29 | }
30 | }
31 | } ~ pathPrefix("search") {
32 | parameters('q) { query =>
33 | get {
34 | complete(movieService.search(query).map(res => OK -> res.asJson))
35 | }
36 | }
37 | } ~ pathPrefix("graph") {
38 | pathEndOrSingleSlash {
39 | get {
40 | complete(movieService.graph(100).map(res => OK -> res.asJson))
41 | }
42 | }
43 | } ~ path("index.html") {
44 | get {
45 | getFromResource("index.html")
46 | }
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/akka-http/src/main/scala/neotypes/exaple/akkahttp/MovieService.scala:
--------------------------------------------------------------------------------
1 | package neotypes.exaple.akkahttp
2 |
3 | import neotypes.Driver
4 | import neotypes.exaple.akkahttp.MovieService.MovieToActor
5 | import neotypes.implicits.syntax.string._
6 | import neotypes.generic.auto._
7 |
8 | import scala.concurrent.Future
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 |
11 | import Boot.Id
12 |
13 | class MovieService(driver: Id[Driver[Future]]) {
14 |
15 | def findMovie(title: String): Future[Option[MovieCast]] =
16 | for {
17 | r <- s"""MATCH (movie:Movie {title: '$title'})
18 | OPTIONAL MATCH (movie)<-[r]-(person:Person)
19 | RETURN movie.title as title, collect({name:person.name, job:head(split(toLower(type(r)),'_')), role:head(r.roles)}) as cast
20 | LIMIT 1""".query[Option[MovieCast]].single(driver)
21 | } yield r
22 |
23 | def search(query: String): Future[Seq[Movie]] =
24 | for {
25 | r <- s"""MATCH (movie:Movie) WHERE toLower(movie.title) CONTAINS '${query.toLowerCase}' RETURN movie"""
26 | .query[Movie]
27 | .list(driver)
28 | } yield r
29 |
30 | def graph(limit: Int = 100): Future[Graph] = {
31 |
32 | val t: Future[List[MovieToActor]] = for {
33 | r <- s"""MATCH (m:Movie)<-[:ACTED_IN]-(a:Person) RETURN m.title as movie, collect(a.name) as cast LIMIT $limit"""
34 | .query[MovieToActor]
35 | .list(driver)
36 | } yield r
37 |
38 | t.map { result =>
39 | val nodes = result.flatMap(toNodes)
40 | val map: Map[Node, Int] = nodes.zipWithIndex.toMap
41 | val rels = result.flatMap { mta =>
42 | val movie = map(Node(mta.movie, MovieService.LABEL_MOVIE))
43 | mta.cast.map(
44 | c =>
45 | Relation(
46 | source = movie,
47 | target = map(Node(c, MovieService.LABEL_ACTOR))
48 | )
49 | )
50 | }
51 |
52 | Graph(nodes, rels)
53 | }
54 | }
55 |
56 | private[this] def toNodes(movieToActor: MovieToActor): Seq[Node] =
57 | movieToActor.cast.map(c => Node(c, MovieService.LABEL_ACTOR)) :+ Node(
58 | movieToActor.movie,
59 | MovieService.LABEL_MOVIE
60 | )
61 | }
62 |
63 | object MovieService {
64 | val LABEL_ACTOR = "actor"
65 | val LABEL_MOVIE = "movie"
66 |
67 | case class MovieToActor(movie: String, cast: List[String])
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/akka-http/src/main/scala/neotypes/exaple/akkahttp/model.scala:
--------------------------------------------------------------------------------
1 | package neotypes.exaple.akkahttp
2 |
3 | case class Cast(name: String, job: String, role: String)
4 |
5 | case class MovieCast(title: String, cast: List[Cast])
6 |
7 | case class Movie(title: String, released: Int, tagline: String)
8 |
9 | case class Graph(nodes: Seq[Node], links: Seq[Relation])
10 |
11 | case class Relation(source: Int, target: Int)
12 |
13 | case class Node(title: String, label: String)
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | val akkaHttpV = "10.2.4"
2 | val scalaTestV = "3.2.8"
3 | val slickVersion = "3.2.3"
4 | val circeV = "0.13.0"
5 | val sttpV = "1.1.13"
6 | val neotypesV = "0.17.0"
7 |
8 | //val commonSettings = Seq(scalaVersion := "2.12.7")
9 | val commonSettings = Seq(scalaVersion := "2.13.5")
10 |
11 | lazy val akkaHttp = (project in file("akka-http"))
12 | .settings(commonSettings: _*)
13 | .settings(
14 | name := "neotypes-cats-effect",
15 | libraryDependencies ++= Seq(
16 | "com.typesafe.akka" %% "akka-http" % akkaHttpV,
17 | "com.typesafe.akka" %% "akka-http-core" % akkaHttpV,
18 | "com.typesafe.akka" %% "akka-actor" % "2.6.14",
19 | "com.typesafe.akka" %% "akka-stream" % "2.6.14",
20 | "ch.megard" %% "akka-http-cors" % "1.1.1",
21 | "com.github.pureconfig" %% "pureconfig" % "0.15.0",
22 | "io.circe" %% "circe-core" % circeV,
23 | "io.circe" %% "circe-generic" % circeV,
24 | "io.circe" %% "circe-parser" % circeV,
25 | // Sugar for serialization and deserialization in akka-http with circe
26 | "de.heikoseeberger" %% "akka-http-circe" % "1.36.0",
27 | "com.dimafeng" %% "neotypes" % neotypesV,
28 | "org.scalatest" %% "scalatest" % scalaTestV % Test,
29 | "org.neo4j.driver" % "neo4j-java-driver" % "4.2.5"
30 | )
31 | )
32 |
--------------------------------------------------------------------------------
/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neotypes/examples/a32801b7585467d48700cdf6ce5db09fd8422704/example.png
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.5.1
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------