├── .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 | ![](example.png) 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 | 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 | 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 | --------------------------------------------------------------------------------