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))