Repository: incubator-s2graph Updated Branches: refs/heads/master 8f9214e82 -> 6a7e58a46
http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/README.md ---------------------------------------------------------------------- diff --git a/s2graphql/README.md b/s2graphql/README.md new file mode 100644 index 0000000..4146e8e --- /dev/null +++ b/s2graphql/README.md @@ -0,0 +1,530 @@ +# Suggest to implement GraphQL as standard web interface for S2Graph. + + - To support GraphQL i used [Akka HTTP](https://github.com/akka/akka-http) and [Sangria](https://github.com/sangria-graphql). each is an HTTP Server and GraphQL Scala implementation. + - I also used [GraphiQL](https://github.com/graphql/graphiql) as a tool for GraphQL queries. + +## Wroking example + + + + + + +## Overview + + The reason I started this work is because the `Label` used by S2Graph has a strong type system, so I think it will work well with the `schema` provided by GraphQL. + + To do this, we converted S2Graph Model (Label, Service ...) into GraphLQL schema whenever added (changed). + +## Setup + Assume that hbase is running on localhost. + If the hbase environment is not set up, you can run it with the following command + +```bash +sbt package +target/apache-s2graph-0.2.1-SNAPSHOT-incubating-bin/bin/hbase-standalone.sh start +``` + +If hbase is running well, run the following command after cloning the project locally. + +`GraphiQL` is not directly related to the `GraphQL` implementation, but is recommended for convenient queries. +Because of the license problem, you should download the file through the following command. + +```bash +cd s2graphql/src/main/resources +wget https://raw.githubusercontent.com/sangria-graphql/sangria-akka-http-example/master/src/main/resources/graphiql.html +``` + +You can see that the `graphiql.html` file is added to the `s2graphql/src/main/resources` folder as shown below. + +``` +$ls +application.conf graphiql.html log4j.properties +``` + +And let's run http server. + +```bash +sbt -DschemaCacheTTL=-1 -Dhttp.port=8000 'project s2graphql' '~re-start' +``` + +When the server is running, connect to `http://localhost:8000`. If it works normally, you can see the following screen. + + + +## API List + - createService + - createLabel + - addEdges + - addEdge + - query (You can recursively browse the linked labels from the service and any other labels that are linked from that label) + +## Your First Grpah (GraphQL version) + +[S2Graph tutorial](https://github.com/apache/incubator-s2graph#your-first-graph) +I have ported the contents of `Your first graph` provided by S2Graph based on GraphQL. + +### Start by connecting to `http://localhost:8000`. + +The environment for this example is Mac OS and Chrome. +You can get help with schema-based `Autocompletion` using the `ctrl + space` key. + +If you add a `label` or `service`, you will need to `refresh` (`cmd + r`) your browser because the schema will change dynamically. + +1. First, we need a name for the new service. + + The following POST query will create a service named "KakaoFavorites". + +Request +```graphql +mutation { + createService( + name: "KakaoFavorites", + compressionAlgorithm: gz + ) { + isSuccess + message + created { + id + } + } +} +``` + +Response +```json +{ + "data": { + "createService": { + "isSuccess": true, + "message": "Created successful", + "created": { + "id": 1 + } + } + } +} +``` + +To make sure the service is created correctly, check out the following. + + > Since the schema has changed, GraphiQL must recognize the changed schema. To do this, refresh the browser several times. + +Request +```graphql +query { + Services(name: KakaoFavorites) { + id + name + } +} +``` + +Response +```json +{ + "data": { + "Services": [ + { + "id": 1, + "name": "KakaoFavorites" + } + ] + } +} +``` + +2. Next, we will need some friends. + + In S2Graph, relationships are organized as labels. Create a label called friends using the following createLabel API call: + +Request + +```graphql +mutation { + createLabel( + name: "friends", + sourceService: { + name: KakaoFavorites, + columnName: "userName", + dataType: string + }, + targetService: { + name: KakaoFavorites, + columnName: "userName", + dataType: string + } + consistencyLevel: strong + ){ + isSuccess + message + created { + id + name + } + } +} +``` + +Response +```json +{ + "data": { + "createLabel": { + "isSuccess": true, + "message": "Created successful", + "created": { + "id": 1, + "name": "friends" + } + } + } +} +``` + +Check if the label has been created correctly +> Since the schema has changed, GraphiQL must recognize the changed schema. To do this, refresh the browser several times. + +Request +```graphql +query { + Labels(name: friends) { + id + name + srcColumnName + tgtColumnName + } +} +``` + +Response +```json +{ + "data": { + "Labels": [ + { + "id": 1, + "name": "friends", + "srcColumnName": "userName", + "tgtColumnName": "userName" + } + ] + } +} +``` + +Now that the label friends is ready, we can store the friendship data. +Entries of a label are called edges, and you can add edges with edges/insert API: + +> Since the schema has changed, GraphiQL must recognize the changed schema. To do this, refresh the browser several times. + +Request +```graphql +mutation { + addEdges( + friends: [ + {from: "Elmo", to: "Big Bird"}, + {from: "Elmo", to: "Ernie"}, + {from: "Elmo", to: "Bert"}, + {from: "Cookie Monster", to: "Grover"}, + {from: "Cookie Monster", to: "Kermit"}, + {from: "Cookie Monster", to: "Oscar"}, + ] + ) { + isSuccess + } +} +``` + +Response +```json +{ + "data": { + "addEdges": [ + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + } + ] + } +} +``` + +Query friends of Elmo with getEdges API: + +Request + +```graphql +query { + KakaoFavorites(id: "Elmo") { + friends { + to + } + } +} +``` + +Response + +```json +{ + "data": { + "KakaoFavorites": [ + { + "friends": [ + { + "to": "Bert" + }, + { + "to": "Ernie" + }, + { + "to": "Big Bird" + } + ] + } + ] + } +} +``` + +Now query friends of Cookie Monster: + +```graphql +query { + KakaoFavorites(id: "Cookie Monster") { + friends { + to + } + } +} +``` + +3. Users of Kakao Favorites will be able to post URLs of their favorite websites. + +Request + +```graphql +mutation { + createLabel( + name: "post", + sourceService: { + name: KakaoFavorites, + columnName: "userName", + dataType: string + }, + targetService: { + name: KakaoFavorites, + columnName: "url", + dataType: string, + } + consistencyLevel: strong + ) { + isSuccess + message + created { + id + name + } + } +} +``` + +Response + +```json +{ + "data": { + "createLabel": { + "isSuccess": true, + "message": "Created successful", + "created": { + "id": 2, + "name": "post" + } + } + } +} +``` + +Now, insert some posts of the users: + +> Since the schema has changed, GraphiQL must recognize the changed schema. To do this, refresh the browser several times. + + +Request + +```graphql +mutation { + addEdges( + post: [ + { from: "Big Bird", to: "www.kakaocorp.com/en/main" }, + { from: "Big Bird", to: "github.com/kakao/s2graph" }, + { from: "Ernie", to: "groups.google.com/forum/#!forum/s2graph" }, + { from: "Grover", to: "hbase.apache.org/forum/#!forum/s2graph" }, + { from: "Kermit", to: "www.playframework.com"}, + { from: "Oscar", to: "www.scala-lang.org"} + ] + ) { + isSuccess + } +} +``` + +Response +```json +{ + "data": { + "addEdges": [ + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + }, + { + "isSuccess": true + } + ] + } +} +``` + +4. So far, we have designed a label schema for the labels friends and post, and stored some edges to them.+ + + This should be enough for creating the timeline feature! The following two-step query will return the URLs for Elmo's timeline, which are the posts of Elmo's friends: + +Request + +```graphql +query { + KakaoFavorites(id: "Elmo") { + friends { + post { + from + to + } + } + } +} +``` + +Response +```json +{ + "data": { + "KakaoFavorites": [ + { + "friends": [ + { + "post": [] + }, + { + "post": [ + { + "from": "Ernie", + "to": "groups.google.com/forum/#!forum/s2graph" + } + ] + }, + { + "post": [ + { + "from": "Big Bird", + "to": "www.kakaocorp.com/en/main" + }, + { + "from": "Big Bird", + "to": "github.com/kakao/s2graph" + } + ] + } + ] + } + ] + } +} +``` + +Also try Cookie Monster's timeline: + +Request +```graphql +query { + KakaoFavorites(id: "Cookie Monster") { + friends { + post { + from + to + } + } + } +} +``` + +Response +```json +{ + "data": { + "KakaoFavorites": [ + { + "friends": [ + { + "post": [ + { + "from": "Oscar", + "to": "www.scala-lang.org" + } + ] + }, + { + "post": [ + { + "from": "Kermit", + "to": "www.playframework.com" + } + ] + }, + { + "post": [ + { + "from": "Grover", + "to": "hbase.apache.org/forum/#!forum/s2graph" + } + ] + } + ] + } + ] + } +} +``` + + + + +The example above is by no means a full blown social network timeline, but it gives you an idea of how to represent, store and query graph data with S2Graph. + http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/build.sbt ---------------------------------------------------------------------- diff --git a/s2graphql/build.sbt b/s2graphql/build.sbt new file mode 100644 index 0000000..d344892 --- /dev/null +++ b/s2graphql/build.sbt @@ -0,0 +1,21 @@ +name := "s2graphql" + +version := "0.1" + +description := "GraphQL server with akka-http and sangria and s2graph" + +scalacOptions ++= Seq("-deprecation", "-feature") + +libraryDependencies ++= Seq( + "org.sangria-graphql" %% "sangria" % "1.3.3", + "org.sangria-graphql" %% "sangria-spray-json" % "1.0.0", + + "com.typesafe.akka" %% "akka-http" % "10.0.10", + "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.10", + + "com.typesafe.akka" %% "akka-slf4j" % "2.4.6", + + "org.scalatest" %% "scalatest" % "3.0.4" % Test +) + +Revolver.settings http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/project/build.properties ---------------------------------------------------------------------- diff --git a/s2graphql/project/build.properties b/s2graphql/project/build.properties new file mode 100644 index 0000000..af73dc4 --- /dev/null +++ b/s2graphql/project/build.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +sbt.version = 0.13.15 http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/project/plugins.sbt ---------------------------------------------------------------------- diff --git a/s2graphql/project/plugins.sbt b/s2graphql/project/plugins.sbt new file mode 100644 index 0000000..4f78e51 --- /dev/null +++ b/s2graphql/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/resources/application.conf ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/resources/application.conf b/s2graphql/src/main/resources/application.conf new file mode 100644 index 0000000..71e8a99 --- /dev/null +++ b/s2graphql/src/main/resources/application.conf @@ -0,0 +1,7 @@ +akka { + loggers = ["akka.event.slf4j.Slf4jLogger"] + event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] + loglevel = "INFO" +} + + http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/resources/log4j.properties ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/resources/log4j.properties b/s2graphql/src/main/resources/log4j.properties new file mode 100644 index 0000000..2070d82 --- /dev/null +++ b/s2graphql/src/main/resources/log4j.properties @@ -0,0 +1,26 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=INFO, A1 + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/scala/GraphQLServer.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/GraphQLServer.scala b/s2graphql/src/main/scala/GraphQLServer.scala new file mode 100644 index 0000000..c2e9ca1 --- /dev/null +++ b/s2graphql/src/main/scala/GraphQLServer.scala @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.s2graph + +import java.util.concurrent.Executors + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.model.StatusCodes._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server._ +import com.typesafe.config.ConfigFactory +import org.apache.s2graph.core.S2Graph +import org.apache.s2graph.core.utils.SafeUpdateCache +import sangria.ast.Document +import sangria.execution._ +import sangria.marshalling.sprayJson._ +import sangria.parser.QueryParser +import sangria.renderer.SchemaRenderer +import sangria.schema.Schema +import spray.json.{JsObject, JsString, JsValue} + +import scala.concurrent.ExecutionContext +import scala.util.{Failure, Success, Try} + +object GraphQLServer { + + // Init s2graph + val numOfThread = Runtime.getRuntime.availableProcessors() + val threadPool = Executors.newFixedThreadPool(numOfThread * 2) + + implicit val ec = ExecutionContext.fromExecutor(threadPool) + + val config = ConfigFactory.load() + val s2graph = new S2Graph(config) + val schemaCacheTTL = Try(config.getInt("schemaCacheTTL")).getOrElse(-1) + val s2Repository = new GraphRepository(s2graph) + val schemaCache = new SafeUpdateCache[Schema[GraphRepository, Any]]("schema", maxSize = 1, ttl = schemaCacheTTL) + + def endpoint(requestJSON: spray.json.JsValue)(implicit e: ExecutionContext): Route = { + + val spray.json.JsObject(fields) = requestJSON + val spray.json.JsString(query) = fields("query") + + val operation = fields.get("operationName") collect { + case spray.json.JsString(op) => op + } + + val vars = fields.get("variables") match { + case Some(obj: spray.json.JsObject) => obj + case _ => spray.json.JsObject.empty + } + + QueryParser.parse(query) match { + case Success(queryAst) => complete(executeGraphQLQuery(queryAst, operation, vars)) + case Failure(error) => complete(BadRequest -> spray.json.JsObject("error" -> JsString(error.getMessage))) + } + } + + /** + * In development mode(schemaCacheTTL = -1), + * a new schema is created for each request. + */ + println(s"schemaCacheTTL: ${schemaCacheTTL}") + + private def createNewSchema(): Schema[GraphRepository, Any] = { + println(s"Schema updated: ${System.currentTimeMillis()}") + + val s2Type = new S2Type(s2Repository) + val newSchema = new SchemaDef(s2Type).S2GraphSchema + + println(SchemaRenderer.renderSchema(newSchema)) + println("-" * 80) + + newSchema + } + + private def executeGraphQLQuery(query: Document, op: Option[String], vars: JsObject)(implicit e: ExecutionContext) = { + val s2schema = schemaCache.withCache("s2Schema")(createNewSchema()) + + Executor.execute( + s2schema, + query, + s2Repository, + variables = vars, + operationName = op + ) + .map((res: spray.json.JsValue) => OK -> res) + .recover { + case error: QueryAnalysisError => BadRequest -> error.resolveError + case error: ErrorWithResolver => InternalServerError -> error.resolveError + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/scala/GraphRepository.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/GraphRepository.scala b/s2graphql/src/main/scala/GraphRepository.scala new file mode 100644 index 0000000..d4c910a --- /dev/null +++ b/s2graphql/src/main/scala/GraphRepository.scala @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.s2graph + +import org.apache.s2graph.S2Type._ +import org.apache.s2graph.core.Management.JsonModel.{Index, Prop} +import org.apache.s2graph.core._ +import org.apache.s2graph.core.mysqls.{Label, LabelIndex, Service, ServiceColumn} +import org.apache.s2graph.core.rest.RequestParser +import org.apache.s2graph.core.storage.MutateResponse +import org.apache.s2graph.core.types.{HBaseType, LabelWithDirection} +import play.api.libs.json._ +import sangria.schema.{Action, Args} + +import scala.concurrent._ +import scala.util.{Failure, Success, Try} + + +/** + * + * @param graph + */ +class GraphRepository(graph: S2GraphLike) { + + val management = graph.management + val parser = new RequestParser(graph) + + implicit val ec = graph.ec + + def partialVertexParamToVertex(column: ServiceColumn, param: PartialVertexParam): S2VertexLike = { + val vid = JSONParser.jsValueToInnerVal(param.vid, column.columnType, column.schemaVersion).get + graph.toVertex(param.service.serviceName, column.columnName, vid) + } + + def partialEdgeParamToS2Edge(labelName: String, param: PartialEdgeParam): S2EdgeLike = { + graph.toEdge( + srcId = param.from, + tgtId = param.to, + labelName = labelName, + props = param.props, + direction = param.direction + ) + } + + def addEdges(args: Args): Future[Seq[MutateResponse]] = { + val edges: Seq[S2EdgeLike] = args.raw.keys.toList.flatMap { labelName => + val params = args.arg[Vector[PartialEdgeParam]](labelName) + params.map(param => partialEdgeParamToS2Edge(labelName, param)) + } + + graph.mutateEdges(edges) + } + + def addEdge(args: Args): Future[Option[MutateResponse]] = { + val edges: Seq[S2EdgeLike] = args.raw.keys.toList.map { labelName => + val param = args.arg[PartialEdgeParam](labelName) + partialEdgeParamToS2Edge(labelName, param) + } + + graph.mutateEdges(edges).map(_.headOption) + } + + def getEdges(vertex: S2VertexLike, label: Label, _dir: String): Future[Seq[S2EdgeLike]] = { + val dir = GraphUtil.directions(_dir) + val labelWithDir = LabelWithDirection(label.id.get, dir) + val step = Step(Seq(QueryParam(labelWithDir))) + val q = Query(Seq(vertex), steps = Vector(step)) + + graph.getEdges(q).map(_.edgeWithScores.map(_.edge)) + } + + def createService(args: Args): Try[Service] = { + val serviceName = args.arg[String]("name") + + Service.findByName(serviceName) match { + case Some(_) => Failure(new RuntimeException(s"Service (${serviceName}) already exists")) + case None => + val cluster = args.argOpt[String]("cluster").getOrElse(parser.DefaultCluster) + val hTableName = args.argOpt[String]("hTableName").getOrElse(s"${serviceName}-${parser.DefaultPhase}") + val preSplitSize = args.argOpt[Int]("preSplitSize").getOrElse(1) + val hTableTTL = args.argOpt[Int]("hTableTTL") + val compressionAlgorithm = args.argOpt[String]("compressionAlgorithm").getOrElse(parser.DefaultCompressionAlgorithm) + + val serviceTry = management + .createService(serviceName, + cluster, + hTableName, + preSplitSize, + hTableTTL, + compressionAlgorithm) + + serviceTry + } + } + + def createLabel(args: Args): Try[Label] = { + val labelName = args.arg[String]("name") + + val srcServiceProp = args.arg[LabelServiceProp]("sourceService") + val tgtServiceProp = args.arg[LabelServiceProp]("targetService") + + val allProps = args.argOpt[Vector[Prop]]("props").getOrElse(Vector.empty) + val indices = args.argOpt[Vector[Index]]("indices").getOrElse(Vector.empty) + + val serviceName = args.argOpt[String]("serviceName").getOrElse(tgtServiceProp.name) + val consistencyLevel = args.argOpt[String]("consistencyLevel").getOrElse("weak") + val hTableName = args.argOpt[String]("hTableName") + val hTableTTL = args.argOpt[Int]("hTableTTL") + val schemaVersion = args.argOpt[String]("schemaVersion").getOrElse(HBaseType.DEFAULT_VERSION) + val isAsync = args.argOpt("isAsync").getOrElse(false) + val compressionAlgorithm = args.argOpt[String]("compressionAlgorithm").getOrElse(parser.DefaultCompressionAlgorithm) + val isDirected = args.argOpt[Boolean]("isDirected").getOrElse(true) + val options = args.argOpt[String]("options") // TODO: support option type + + val labelTry: scala.util.Try[Label] = management.createLabel( + labelName, + srcServiceProp.name, + srcServiceProp.columnName, + srcServiceProp.dataType, + tgtServiceProp.name, + tgtServiceProp.columnName, + tgtServiceProp.dataType, + isDirected, + serviceName, + indices, + allProps, + consistencyLevel, + hTableName, + hTableTTL, + schemaVersion, + isAsync, + compressionAlgorithm, + options + ) + + labelTry + } + + def allServices: List[Service] = Service.findAll() + + def findServiceByName(name: String): Option[Service] = Service.findByName(name) + + def allLabels: List[Label] = Label.findAll() + + def findLabelByName(name: String): Option[Label] = Label.findByName(name) + +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/scala/HttpServer.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/HttpServer.scala b/s2graphql/src/main/scala/HttpServer.scala new file mode 100644 index 0000000..cf1cc0c --- /dev/null +++ b/s2graphql/src/main/scala/HttpServer.scala @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.s2graph + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.stream.ActorMaterializer +import akka.http.scaladsl.server._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ + +import Console._ +import scala.concurrent.Await +import scala.language.postfixOps + +object Server extends App { + + implicit val actorSystem = ActorSystem("s2graphql-server") + implicit val materializer = ActorMaterializer() + + import actorSystem.dispatcher + import scala.concurrent.duration._ + + println("Starting GRAPHQL server...") + + val route: Route = + (post & path("graphql")) { + entity(as[spray.json.JsValue])(GraphQLServer.endpoint) + } ~ { + getFromResource("graphiql.html") + } + + val port = sys.props.get("http.port").fold(8000)(_.toInt) + Http().bindAndHandle(route, "0.0.0.0", port) + + + def shutdown(): Unit = { + println("Terminating...") + actorSystem.terminate() + Await.result(actorSystem.whenTerminated, 10 seconds) + + println("Terminated.") + } +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/scala/S2Type.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/S2Type.scala b/s2graphql/src/main/scala/S2Type.scala new file mode 100644 index 0000000..59eae4c --- /dev/null +++ b/s2graphql/src/main/scala/S2Type.scala @@ -0,0 +1,519 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.s2graph + +import org.apache.s2graph.core.Management.JsonModel.{Index, Prop} +import org.apache.s2graph.core._ +import org.apache.s2graph.core.mysqls._ +import org.apache.s2graph.core.storage.MutateResponse +import play.api.libs.json.JsValue +import sangria.marshalling.{CoercedScalaResultMarshaller, FromInput} +import sangria.schema._ + +import scala.language.existentials +import scala.util.{Failure, Success, Try} + +object S2Type { + + import sangria.schema._ + + case class LabelServiceProp(name: String, columnName: String, dataType: String) + + case class MutationResponse[T](result: Try[T]) + + case class PartialVertexParam(service: Service, vid: JsValue) + + case class PartialEdgeParam(ts: Long, + from: Any, + to: Any, + direction: String, + props: Map[String, Any]) + + implicit object PartialEdgeFromInput extends FromInput[PartialEdgeParam] { + val marshaller = CoercedScalaResultMarshaller.default + + def fromResult(node: marshaller.Node) = { + val inputMap = node.asInstanceOf[Map[String, Any]] + + val from = inputMap("from") + val to = inputMap("to") + + val ts = inputMap.get("timestamp") match { + case Some(Some(v)) => v.asInstanceOf[Long] + case _ => System.currentTimeMillis() + } + + val dir = inputMap.get("direction") match { + case Some(Some(v)) => v.asInstanceOf[String] + case _ => "out" + } + + val props = inputMap.get("props") match { + case Some(Some(v)) => v.asInstanceOf[Map[String, Option[Any]]].filter(_._2.isDefined).mapValues(_.get) + case _ => Map.empty[String, Any] + } + + PartialEdgeParam(ts, from, to, dir, props) + } + } + + implicit object IndexFromInput extends FromInput[Index] { + val marshaller = CoercedScalaResultMarshaller.default + + def fromResult(node: marshaller.Node) = { + val input = node.asInstanceOf[Map[String, Any]] + Index(input("name").asInstanceOf[String], input("propNames").asInstanceOf[Seq[String]]) + } + } + + implicit object PropFromInput extends FromInput[Prop] { + val marshaller = CoercedScalaResultMarshaller.default + + def fromResult(node: marshaller.Node) = { + val input = node.asInstanceOf[Map[String, String]] + Prop(input("name"), input("defaultValue"), input("dataType")) + } + } + + implicit object LabelServiceFromInput extends FromInput[LabelServiceProp] { + val marshaller = CoercedScalaResultMarshaller.default + + def fromResult(node: marshaller.Node) = { + val input = node.asInstanceOf[Map[String, String]] + LabelServiceProp(input("name"), input("columnName"), input("dataType")) + } + } + + def s2TypeToScalarType(from: String): ScalarType[_] = from match { + case "string" => StringType + case "int" => IntType + case "integer" => IntType + case "long" => LongType + case "float" => FloatType + case "double" => FloatType + case "boolean" => BooleanType + case "bool" => BooleanType + } +} + +class S2Type(repo: GraphRepository) { + + import sangria.macros.derive._ + import S2Type._ + + lazy val DirArg = Argument("direction", OptionInputType(DirectionType), "desc here", defaultValue = "out") + + lazy val NameArg = Argument("name", StringType, description = "desc here") + + lazy val ServiceNameArg = Argument("name", OptionInputType(ServiceListType), description = "desc here") + + lazy val LabelNameArg = Argument("name", OptionInputType(LabelListType), description = "desc here") + + lazy val PropArg = Argument("props", OptionInputType(ListInputType(InputPropType)), description = "desc here") + + lazy val IndicesArg = Argument("indices", OptionInputType(ListInputType(InputIndexType)), description = "desc here") + + lazy val ServiceType = deriveObjectType[GraphRepository, Service]( + ObjectTypeName("Service"), + ObjectTypeDescription("desc here"), + RenameField("serviceName", "name") + ) + + lazy val LabelMetaType = deriveObjectType[GraphRepository, LabelMeta]( + ObjectTypeName("LabelMeta"), + ExcludeFields("seq", "labelId") + ) + + lazy val DataTypeType = EnumType( + "DataType", + description = Option("desc here"), + values = List( + EnumValue("string", value = "string"), + EnumValue("int", value = "int"), + EnumValue("long", value = "long"), + EnumValue("float", value = "float"), + EnumValue("boolean", value = "boolean") + ) + ) + + lazy val DirectionType = EnumType( + "Direction", + description = Option("desc here"), + values = List( + EnumValue("out", value = "out"), + EnumValue("in", value = "in") + ) + ) + + lazy val InputIndexType = InputObjectType[Index]( + "Index", + description = "desc here", + fields = List( + InputField("name", StringType), + InputField("propNames", ListInputType(StringType)) + ) + ) + + lazy val InputPropType = InputObjectType[Prop]( + "Prop", + description = "desc here", + fields = List( + InputField("name", StringType), + InputField("dataType", DataTypeType), + InputField("defaultValue", StringType) + ) + ) + + lazy val dummyEnum = EnumValue("_", value = "_") + + lazy val ServiceListType = EnumType( + s"ServiceList", + description = Option("desc here"), + values = + dummyEnum +: repo.allServices.map { service => + EnumValue(service.serviceName, value = service.serviceName) + } + ) + + lazy val LabelListType = EnumType( + s"LabelList", + description = Option("desc here"), + values = + dummyEnum +: repo.allLabels.map { label => + EnumValue(label.label, value = label.label) + } + ) + + lazy val CompressionAlgorithmType = EnumType( + "CompressionAlgorithm", + description = Option("desc here"), + values = List( + EnumValue("gz", description = Option("desc here"), value = "gz"), + EnumValue("lz4", description = Option("desc here"), value = "lz4") + ) + ) + + lazy val ConsistencyLevelType = EnumType( + "ConsistencyList", + description = Option("desc here"), + values = List( + EnumValue("weak", description = Option("desc here"), value = "weak"), + EnumValue("strong", description = Option("desc here"), value = "strong") + ) + ) + + lazy val InputLabelServiceType = InputObjectType[LabelServiceProp]( + "LabelServiceProp", + description = "desc here", + fields = List( + InputField("name", ServiceListType), + InputField("columnName", StringType), + InputField("dataType", DataTypeType) + ) + ) + + lazy val LabelIndexType = deriveObjectType[GraphRepository, LabelIndex]( + ObjectTypeName("LabelIndex"), + ObjectTypeDescription("desc here"), + ExcludeFields("seq", "metaSeqs", "formulars", "labelId") + ) + + lazy val LabelType = deriveObjectType[GraphRepository, Label]( + ObjectTypeName("Label"), + ObjectTypeDescription("desc here"), + AddFields( + Field("indexes", ListType(LabelIndexType), resolve = c => Nil), + Field("props", ListType(LabelMetaType), resolve = c => Nil) + ), + RenameField("label", "name") + ) + + def makeInputPartialEdgeParamType(label: Label): InputObjectType[PartialEdgeParam] = { + lazy val InputPropsType = InputObjectType[Map[String, ScalarType[_]]]( + s"${label.label}_props", + description = "desc here", + () => label.labelMetaSet.toList.map { lm => + InputField(lm.name, OptionInputType(s2TypeToScalarType(lm.dataType))) + } + ) + + lazy val labelFields = List( + InputField("timestamp", OptionInputType(LongType)), + InputField("from", s2TypeToScalarType(label.srcColumnType)), + InputField("to", s2TypeToScalarType(label.srcColumnType)), + InputField("direction", OptionInputType(DirectionType)) + ) + + InputObjectType[PartialEdgeParam]( + s"${label.label}_mutate", + description = "desc here", + () => + if (label.labelMetaSet.isEmpty) labelFields + else labelFields ++ Seq(InputField("props", OptionInputType(InputPropsType))) + ) + } + + lazy val EdgeArg = repo.allLabels.map { label => + val inputPartialEdgeParamType = makeInputPartialEdgeParamType(label) + Argument(label.label, OptionInputType(inputPartialEdgeParamType)) + } + + lazy val EdgesArg = repo.allLabels.map { label => + val inputPartialEdgeParamType = makeInputPartialEdgeParamType(label) + Argument(label.label, OptionInputType(ListInputType(inputPartialEdgeParamType))) + } + + lazy val serviceOptArgs = List( + "compressionAlgorithm" -> CompressionAlgorithmType, + "cluster" -> StringType, + "hTableName" -> StringType, + "preSplitSize" -> IntType, + "hTableTTL" -> IntType + ).map { case (name, _type) => Argument(name, OptionInputType(_type)) } + + lazy val labelRequiredArg = List( + "sourceService" -> InputLabelServiceType, + "targetService" -> InputLabelServiceType + ).map { case (name, _type) => Argument(name, _type) } + + lazy val labelOptsArgs = List( + "serviceName" -> ServiceListType, + "consistencyLevel" -> ConsistencyLevelType, + "isDirected" -> BooleanType, + "isAsync" -> BooleanType, + "schemaVersion" -> StringType + ).map { case (name, _type) => Argument(name, OptionInputType(_type)) } + + lazy val ServiceMutationResponseType = makeMutationResponseType[Service]( + "CreateService", + "desc here", + ServiceType + ) + + lazy val LabelMutationResponseType = makeMutationResponseType[Label]( + "CreateLabel", + "desc here", + LabelType + ) + + lazy val EdgeMutateResponseType = deriveObjectType[GraphRepository, MutateResponse]( + ObjectTypeName("EdgeMutateResponse"), + ObjectTypeDescription("desc here"), + AddFields( + Field("isSuccess", BooleanType, resolve = c => c.value.isSuccess) + ) + ) + + def makeMutationResponseType[T](name: String, desc: String, tpe: ObjectType[_, T]) = { + ObjectType( + name, + desc, + () => fields[Unit, MutationResponse[T]]( + Field("isSuccess", + BooleanType, + resolve = _.value.result.isSuccess + ), + Field("message", + StringType, + resolve = _.value.result match { + case Success(_) => s"Created successful" + case Failure(ex) => ex.getMessage + } + ), + Field("created", + OptionType(tpe), + resolve = _.value.result.toOption + ) + ) + ) + } + + lazy val vertexIdField: Field[GraphRepository, Any] = Field( + "id", + PlayJsonPolyType.PolyType, + description = Some("desc here"), + resolve = _.value match { + case v: PartialVertexParam => v.vid + case _ => throw new RuntimeException("dead code") + } + ) + + lazy val tsField: Field[GraphRepository, Any] = + Field("timestamp", + LongType, + description = Option("desc here"), + resolve = _.value match { + case e: S2EdgeLike => e.ts + case _ => throw new RuntimeException("dead code") + }) + + def makeEdgePropFields(edgeFieldNameWithTypes: List[(String, String)]): List[Field[GraphRepository, Any]] = { + def makeField[A](name: String, cType: String, tpe: ScalarType[A]): Field[GraphRepository, Any] = + Field(name, OptionType(tpe), description = Option("desc here"), resolve = _.value match { + case e: S2EdgeLike => + val innerVal = name match { + case "from" => e.srcForVertex.innerId + case "to" => e.tgtForVertex.innerId + case _ => e.propertyValue(name).get.innerVal + } + + JSONParser.innerValToAny(innerVal, cType).asInstanceOf[A] + + case _ => throw new RuntimeException("dead code") + }) + + edgeFieldNameWithTypes.map { case (cName, cType) => + cType match { + case "boolean" | "bool" => makeField[Boolean](cName, cType, BooleanType) + case "string" | "str" | "s" => makeField[String](cName, cType, StringType) + case "int" | "integer" | "i" | "int32" | "integer32" => makeField[Int](cName, cType, IntType) + case "long" | "l" | "int64" | "integer64" => makeField[Long](cName, cType, LongType) + case "double" | "d" | "float64" | "float" | "f" | "float32" => makeField[Double](cName, cType, FloatType) + case _ => throw new RuntimeException(s"Cannot support data type: ${cType}") + } + } + } + + // ex: KakaoFavorites + lazy val serviceVertexFields: List[Field[GraphRepository, Any]] = repo.allServices.map { service => + val serviceId = service.id.get + val connectedLabels = repo.allLabels.filter { lb => + lb.srcServiceId == serviceId || lb.tgtServiceId == serviceId + }.distinct + + // label connected on services, friends, post + lazy val connectedLabelFields: List[Field[GraphRepository, Any]] = connectedLabels.map { label => + val labelColumns = List("from" -> label.srcColumnType, "to" -> label.tgtColumnType) + val labelProps = label.labelMetas.map { lm => lm.name -> lm.dataType } + + lazy val EdgeType = ObjectType(label.label, () => fields[GraphRepository, Any](edgeFields ++ connectedLabelFields: _*)) + lazy val edgeFields: List[Field[GraphRepository, Any]] = tsField :: makeEdgePropFields(labelColumns ++ labelProps) + lazy val edgeTypeField: Field[GraphRepository, Any] = Field( + label.label, + ListType(EdgeType), + arguments = DirArg :: Nil, + description = Some("edges"), + resolve = { c => + val dir = c.argOpt("direction").getOrElse("out") + + val vertex: S2VertexLike = c.value match { + case v: S2VertexLike => v + case e: S2Edge => if (dir == "out") e.tgtVertex else e.srcVertex + case vp: PartialVertexParam => + if (dir == "out") c.ctx.partialVertexParamToVertex(label.tgtColumn, vp) + else c.ctx.partialVertexParamToVertex(label.srcColumn, vp) + } + + c.ctx.getEdges(vertex, label, dir) + } + ) + + edgeTypeField + } + + lazy val VertexType = ObjectType( + s"${service.serviceName}", + fields[GraphRepository, Any](vertexIdField +: connectedLabelFields: _*) + ) + + Field( + service.serviceName, + ListType(VertexType), + arguments = List( + Argument("id", OptionInputType(PlayJsonPolyType.PolyType)), + Argument("ids", OptionInputType(ListInputType(PlayJsonPolyType.PolyType))) + ), + description = Some(s"serviceName: ${service.serviceName}"), + resolve = { c => + val id = c.argOpt[JsValue]("id").toSeq + val ids = c.argOpt[List[JsValue]]("ids").toList.flatten + val svc = c.ctx.findServiceByName(service.serviceName).get + + (id ++ ids).map { vid => PartialVertexParam(svc, vid) } + } + ): Field[GraphRepository, Any] + } + + lazy val serviceField: Field[GraphRepository, Any] = Field( + "Services", + ListType(ServiceType), + description = Option("desc here"), + arguments = List(ServiceNameArg), + resolve = { c => + c.argOpt[String]("name") match { + case Some(name) => c.ctx.allServices.filter(_.serviceName == name) + case None => c.ctx.allServices + } + } + ) + + lazy val labelField: Field[GraphRepository, Any] = Field( + "Labels", + ListType(LabelType), + description = Option("desc here"), + arguments = List(LabelNameArg), + resolve = { c => + c.argOpt[String]("name") match { + case Some(name) => c.ctx.allLabels.filter(_.label == name) + case None => c.ctx.allLabels + } + } + ) + + /** + * Query fields + * Provide s2graph query API + * + * - Fields is created(or changed) for metadata is changed. + */ + lazy val queryFields = Seq(serviceField, labelField) ++ serviceVertexFields + + /** + * Mutation fields + * Provide s2graph management API + * + * - createService + * - createLabel + * - addEdge + * - addEdges + */ + lazy val mutationFields: List[Field[GraphRepository, Any]] = List( + Field("createService", + ServiceMutationResponseType, + arguments = NameArg :: serviceOptArgs, + resolve = c => MutationResponse(c.ctx.createService(c.args)) + ), + Field("createLabel", + LabelMutationResponseType, + arguments = NameArg :: PropArg :: IndicesArg :: labelRequiredArg ::: labelOptsArgs, + resolve = c => MutationResponse(c.ctx.createLabel(c.args)) + ), + Field("addEdge", + OptionType(EdgeMutateResponseType), + arguments = EdgeArg, + resolve = c => c.ctx.addEdge(c.args) + ), + Field("addEdges", + ListType(EdgeMutateResponseType), + arguments = EdgesArg, + resolve = c => c.ctx.addEdges(c.args) + ) + ) +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/scala/SangriaPlayJsonScalarType.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/SangriaPlayJsonScalarType.scala b/s2graphql/src/main/scala/SangriaPlayJsonScalarType.scala new file mode 100644 index 0000000..8421b8a --- /dev/null +++ b/s2graphql/src/main/scala/SangriaPlayJsonScalarType.scala @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.s2graph + +import sangria.ast._ +import sangria.schema._ +import sangria.validation.ValueCoercionViolation + +// https://gist.github.com/OlegIlyenko/5b96f4b54f656aac226d3c4bc33fd2a6 + +object PlayJsonPolyType { + + import sangria.ast + import sangria.schema._ + import play.api.libs.json._ + + case object JsonCoercionViolation extends ValueCoercionViolation("Not valid JSON") + + def scalarTypeToJsValue(v: sangria.ast.Value): JsValue = v match { + case v: IntValue => JsNumber(v.value) + case v: BigIntValue => JsNumber(BigDecimal(v.value.bigInteger)) + case v: FloatValue => JsNumber(v.value) + case v: BigDecimalValue => JsNumber(v.value) + case v: StringValue => JsString(v.value) + case v: BooleanValue => JsBoolean(v.value) + case v: ListValue => JsNull + case v: VariableValue => JsNull + case v: NullValue => JsNull + case v: ObjectValue => JsNull + } + + implicit val PolyType = ScalarType[JsValue]("Poly", + description = Some("Type Poly = String | Number | Boolean"), + coerceOutput = (value, _) â value match { + case JsString(s) => s + case JsNumber(n) => n + case JsBoolean(b) => b + case JsNull => null + case _ => value + }, + coerceUserInput = { + case v: String => Right(JsString(v)) + case v: Boolean => Right(JsBoolean(v)) + case v: Int => Right(JsNumber(v)) + case v: Long => Right(JsNumber(v)) + case v: Float => Right(JsNumber(v.toDouble)) + case v: Double => Right(JsNumber(v)) + case v: BigInt => Right(JsNumber(BigDecimal(v))) + case v: BigDecimal => Right(JsNumber(v)) + case _ => Left(JsonCoercionViolation) + }, + coerceInput = { + case value: ast.StringValue => Right(JsString(value.value)) + case value: ast.IntValue => Right(JsNumber(value.value)) + case value: ast.FloatValue => Right(JsNumber(value.value)) + case value: ast.BigIntValue => Right(JsNumber(BigDecimal(value.value.bigInteger))) + case _ => Left(JsonCoercionViolation) + }) +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/main/scala/SchemaDef.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/SchemaDef.scala b/s2graphql/src/main/scala/SchemaDef.scala new file mode 100644 index 0000000..6829040 --- /dev/null +++ b/s2graphql/src/main/scala/SchemaDef.scala @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.s2graph + +/** + * S2Graph GraphQL schema. + * + * When a Label or Service is created, the GraphQL schema is created dynamically. + */ +class SchemaDef(s2Type: S2Type) { + + import sangria.schema._ + + val S2QueryType = ObjectType[GraphRepository, Any]("Query", fields(s2Type.queryFields: _*)) + + val S2MutationType = ObjectType[GraphRepository, Any]("Mutation", fields(s2Type.mutationFields: _*)) + + val S2GraphSchema = Schema(S2QueryType, Option(S2MutationType)) +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2graphql/src/test/resources/application.conf ---------------------------------------------------------------------- diff --git a/s2graphql/src/test/resources/application.conf b/s2graphql/src/test/resources/application.conf new file mode 100644 index 0000000..e69de29 http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/1f1dee3d/s2rest_play/app/org/apache/s2graph/rest/play/controllers/AdminController.scala ---------------------------------------------------------------------- diff --git a/s2rest_play/app/org/apache/s2graph/rest/play/controllers/AdminController.scala b/s2rest_play/app/org/apache/s2graph/rest/play/controllers/AdminController.scala index 2d3530b..2ee6395 100644 --- a/s2rest_play/app/org/apache/s2graph/rest/play/controllers/AdminController.scala +++ b/s2rest_play/app/org/apache/s2graph/rest/play/controllers/AdminController.scala @@ -304,20 +304,20 @@ object AdminController extends Controller { * @param columnName * @return */ - def addServiceColumnProp(serviceName: String, columnName: String) = Action(parse.json) { request => - addServiceColumnPropInner(serviceName, columnName)(request.body) match { + def addServiceColumnProp(serviceName: String, columnName: String, storeInGlobalIndex: Boolean = false) = Action(parse.json) { request => + addServiceColumnPropInner(serviceName, columnName, storeInGlobalIndex)(request.body) match { case None => bad(s"can`t find service with $serviceName or can`t find serviceColumn with $columnName") case Some(m) => Ok(m.toJson).as(applicationJsonHeader) } } - def addServiceColumnPropInner(serviceName: String, columnName: String)(js: JsValue) = { + def addServiceColumnPropInner(serviceName: String, columnName: String, storeInGlobalIndex: Boolean = false)(js: JsValue) = { for { service <- Service.findByName(serviceName) serviceColumn <- ServiceColumn.find(service.id.get, columnName) prop <- requestParser.toPropElements(js).toOption } yield { - ColumnMeta.findOrInsert(serviceColumn.id.get, prop.name, prop.datatType) + ColumnMeta.findOrInsert(serviceColumn.id.get, prop.name, prop.dataType, storeInGlobalIndex) } }
