ScalliGraph is a framework for web applications using graph database.
- Reduce boilerplate code as much as possible.
- gremlin DSL is used to access the database. Application doesn't require code specific to the database engine.
- type safe
- Database schema generation
- GraphQL
Currently, there is no official release of ScalliGraph. You can wait the first release and add the dependency in your build file:
libraryDependencies += "org.thehive-project" %% "scalligraph" % "0.1.0"
or use ScalliGraph sources in your project:
lazy val scalligraph = (project in file("path/to/scalligraph")) .settings(name := "scalligraph") lazy val myApplication = (project in file(".")) .dependsOn(scalligraph)
ScalliGraph uses macros to reduce boilerplate code. The macro paradise compiler plugins must be enabled:
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
Database schema is done by defining case classes and by annotate them with
@VertexEntity
for vertex or @EdgeEntity[From, To]
for edge. ScalliGraph
inspects these classes and generate database schema and CRUD methods.
import org.thp.scalligraph.models.{EdgeEntity, VertexEntity} @VertexEntity case class Person(name: String, age: Int) @VertexEntity case class Software(name: String, lang: String) @EdgeEntity[Person, Person] case class Knows(weight: Double) @EdgeEntity[Person, Software] case class Created(weight: Double)
The recognized types for model fields are String
, Long
, Int
, Date
,
Boolean
, Double
, Float
and JsObject
. Field can be Option
and Seq
of theses types.
If it is not enough, you can create your own mapping by add implicit UniMapping
value of annotate the field with @WithMapping
For each entity (vertex and edge) you may have a service class that defines what you can do with: CRUD. It also has a traversal to query your entities using Gremlin DSL.
Default service class already defines these methods. You can of course override them.
class PersonSrv(implicit db: Database) extends VertexSrv[Person] { // Add business operations on Person override def steps(implicit graph: Graph): PersonSteps = new PersonSteps(graph.V.hasLabel(model.label)) }
Create method accepts model class and returns the same class with Entity
trait. This trait contains meta data, common to persisted vertex and edge:
_id
, _createdAt
, _createdBy
, _updatedAt
, _updatedBy
.
The steps
method returns a Gremlin traversal which can be enriched.
ScalliGrah uses gremlin-scala. You can have more details on how to write query
on gremlin-scala home page.
@EntitySteps[Person] class PersonSteps(raw: GremlinScala[Vertex])(implicit db: Database) extends BaseVertexSteps[Person, PersonSteps](raw) { def created = new SoftwareSteps(raw.out("Created")) def knownPerson: List[Person] = raw.out("Knows").toList }
With the annotation @EntitySteps
, ScalliGraph add a method for each field of
your model which returns a traversal of that field value.
personSteps.age.max.head
returns the age of the oldest person.
A controller method consists of extracting data from HTTP request, check user permissions, call service layer and marshall the result.
ScalliGraph offers DSL to build a controller:
apiMethod("create a person") .extract('person, FieldsParser[Person]) // Extract person from HTTP request .extract('friends, FieldsParser[String].sequence.on("friends")) // Extract a string under the name "friends" .requires(Permissions.write) { implicit request ⇒ // Check user authentication and verify if (s)he has the write permission // request is the HTTP request (play.api.mvc.Request) with authentication information (AuthContext) db.transaction { implicit graph ⇒ // Start a new transaction val person = request.body('person) // retrive the extracted data from the HTTP request // Note that the type of person is the case class Person val friends = request.body('friends) // Seq[String] val createdPerson = personSrv.create(person) friends .map(personSrv.get) // get person from id .foreach(person ⇒ knowsSrv.create(Knows(1), createdPerson, person)) // then create edges Results.Created } }
More details will come ...
Data is requested using a query chain. In your application, you can describe
all possible links which must a subclass of ParamQuery
. The object Query
contains convenient method to create ParamQuery
.
You should also declare all public properties of your data. These properties are used to build filter queries, sort queries and GraphQL schema.
Links and public properties are put in a QueryExecutor. The QueryExecutor is able to parse and execute a query from a HTTP request.
class ModernQueryExecutor extends QueryExecutor { val personSrv = new PersonSrv val softwareSrv = new SoftwareSrv override val publicProperties = PublicPropertyListBuilder[PersonSteps, Vertex] .property[String]("createdBy").derived(_ ⇒ _.value[String]("_createdBy")) .property[String]("label").derived(_ ⇒ _.value[String]("name").map("Mister " + _)) .property[String]("name").simple .property[Int]("age").simple .build ::: PublicPropertyListBuilder[SoftwareSteps, Vertex] .property[String]("createdBy").derived(_ ⇒ _.value[String]("_createdBy")) .property[String]("name").simple .property[String]("lang").simple .property[String]("any") .seq(_ ⇒ Seq( _.value[String]("_createdBy"), _.value[String]("name"), _.value[String]("lang") )) .build override val queries = Seq( Query.init[PersonSteps]("allPeople", (graph, _) => personSrv.initSteps(graph)), Query.init[SoftwareSteps]("allSoftware", (graph, _) => softwareSrv.initSteps(graph)), Query.initWithParam[SeniorAgeThreshold, PersonSteps]("seniorPeople", { (seniorAgeThreshold, graph, _) ⇒ personSrv.initSteps(graph).where(_.has(Key[Int]("age"), P.gte(seniorAgeThreshold.age))) }), Query[PersonSteps, SoftwareSteps]("created", (personSteps, _) => personSteps.created), Query.withParam[FriendLevel, PersonSteps, PersonSteps]("friends", (friendLevel, personSteps, _) ⇒ personSteps.friends(friendLevel.level)), Query[Person with Entity, Output[OutputPerson]]("output", (person, _) ⇒ person), Query[Software with Entity, Output[OutputSoftware]]("output", (software, _) ⇒ software) )
Once described, query can be parsed from HTTP request then it can be executed
db.transaction { graph ⇒ val query: Query Or Every[AttributeError] = modernQueryExecutor.parser(Field(request)) val result: JsValue = modernQueryExecutor.execute(query, graph, authContext).toJson }
HTTP request body is a list of query elements:
{ "query": [ { "_name": "allPeople" }, { "_name": "filter", "_and": [ { "_lt": { "age": 30 } }, { "_contains": { "name": "a" } } ]}, { "_name": "created" }, { "_name": "_toList" } ] }
From a QueryExecutor, Scalligraph can generate the related GraphQL schema and execute the query:
import sangria.schema.{Schema, } import sangria.parser.QueryParser val query: Document = QueryParser.parse(inputQueryString).get val schema: Schema[AuthGraph, Unit] = SchemaGenerator(modernQueryExecutor) val result: Future[JsValue] = Executor.execute(schema, query, AuthGraph(Some(authContext), graph))