| package schema |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "strings" |
| |
| "github.com/golang/glog" |
| "github.com/itaysk/regogo" |
| "github.com/vektah/gqlparser" |
| "github.com/vektah/gqlparser/ast" |
| "github.com/vektah/gqlparser/formatter" |
| //"github.com/bradleyjkemp/memviz" |
| ) |
| |
| const jsonContentType = "application/json" |
| const textContentType = "text/plain" |
| |
| // TODO(giolekva): escape |
| const getSchemaQuery = `{ getGQLSchema() { schema generatedSchema } }` |
| const setSchemaQuery = `mutation { updateGQLSchema(input: { set: { schema: "%s" } }) { gqlSchema { schema generatedSchema } } }` |
| const runQuery = `{ "query": "%s" }` |
| const eventTmpl = ` |
| type %sEvent { |
| id: ID! |
| state: EventState! @search(by: [exact]) |
| node: %s! @hasInverse(field: events) |
| } |
| |
| extend type %s { |
| events: [%sEvent] @hasInverse(field: node) |
| }` |
| |
| type DgraphClient struct { |
| gqlAddress string |
| schemaAddress string |
| userSchema string |
| generatedSchema string |
| schema *ast.Schema |
| } |
| |
| func NewDgraphClient(gqlAddress, schemaAddress string) (GraphQLClient, error) { |
| ret := &DgraphClient{ |
| gqlAddress: gqlAddress, |
| schemaAddress: schemaAddress, |
| userSchema: "", |
| generatedSchema: ""} |
| if err := ret.fetchSchema(); err != nil { |
| return nil, err |
| } |
| return ret, nil |
| } |
| |
| func (s *DgraphClient) Schema() *ast.Schema { |
| return s.schema |
| } |
| |
| func (s *DgraphClient) AddSchema(gqlSchema string) error { |
| extendedSchema, err := s.extendSchema(gqlSchema) |
| if err != nil { |
| return err |
| } else { |
| return s.SetSchema(extendedSchema) |
| } |
| } |
| |
| func (s *DgraphClient) SetSchema(gqlSchema string) error { |
| glog.Info("Setting GraphQL schema") |
| glog.Info(gqlSchema) |
| resp, err := s.runQuery( |
| fmt.Sprintf(setSchemaQuery, gqlSchema), |
| s.schemaAddress) |
| if err != nil { |
| return err |
| } |
| data, err := regogo.Get(resp, "input.updateGQLSchema.gqlSchema") |
| if err != nil { |
| return err |
| } |
| return s.updateSchema(data.JSON()) |
| } |
| |
| func (s *DgraphClient) fetchSchema() error { |
| glog.Infof("Getting GraphQL schema") |
| resp, err := s.runQuery(getSchemaQuery, s.schemaAddress) |
| if err != nil { |
| return err |
| } |
| data, err := regogo.Get(resp, "input.getGQLSchema") |
| if err != nil { |
| return err |
| } |
| return s.updateSchema(data.JSON()) |
| } |
| |
| func (s *DgraphClient) updateSchema(resp string) error { |
| userSchema, err := regogo.Get(resp, "input.schema") |
| if err != nil { |
| return err |
| } |
| generatedSchema, err := regogo.Get(resp, "input.generatedSchema") |
| if err != nil { |
| return err |
| } |
| schema, gqlErr := gqlparser.LoadSchema(&ast.Source{Input: generatedSchema.String()}) |
| if gqlErr != nil { |
| return gqlErr |
| } |
| s.userSchema = userSchema.String() |
| s.generatedSchema = generatedSchema.String() |
| s.schema = schema |
| return nil |
| } |
| |
| func (s *DgraphClient) RunQuery(query string) (string, error) { |
| q, gqlErr := gqlparser.LoadQuery(s.Schema(), query) |
| if gqlErr != nil { |
| return "", errors.New(gqlErr.Error()) |
| } |
| rewritten := rewriteQuery(q, s.Schema()) |
| var b strings.Builder |
| // TODO(giolekva): gqlparser should be reporting error back |
| formatter.NewFormatter(&b).FormatQueryDocument(rewritten) |
| query = b.String() |
| return s.runQuery(query, s.gqlAddress) |
| } |
| |
| func (s *DgraphClient) runQuery(query string, onAddr string) (string, error) { |
| glog.Infof("Running GraphQL query: %s", query) |
| queryJson := fmt.Sprintf(runQuery, fixWhitespaces(escapeQuery(query))) |
| resp, err := http.Post( |
| onAddr, |
| jsonContentType, |
| bytes.NewReader([]byte(queryJson))) |
| if err != nil { |
| return "", err |
| } |
| respBody, err := ioutil.ReadAll(resp.Body) |
| if err != nil { |
| return "", err |
| } |
| respStr := string(respBody) |
| glog.Infof("Result: %s", string(respStr)) |
| // errStr, err := regogo.Get(respStr, "input.errors") |
| // if err == nil { |
| // return "", errors.New(errStr.JSON()) |
| // } |
| data, err := regogo.Get(respStr, "input.data") |
| if err != nil { |
| return "", err |
| } |
| return data.JSON(), nil |
| } |
| |
| func (s *DgraphClient) extendSchema(schema string) (string, error) { |
| try := s.generatedSchema + " " + schema |
| parsed, gqlErr := gqlparser.LoadSchema(&ast.Source{Input: try}) |
| if gqlErr != nil { |
| return "", errors.New(gqlErr.Error()) |
| } |
| var extended strings.Builder |
| _, err := io.WriteString(&extended, s.userSchema) |
| if err != nil { |
| return "", err |
| } |
| _, err = io.WriteString(&extended, schema) |
| if err != nil { |
| return "", err |
| } |
| for _, t := range parsed.Types { |
| if shouldIgnoreDefinition(t) { |
| continue |
| } |
| if _, ok := parsed.Types[t.Name+"Event"]; ok { |
| continue |
| } |
| _, err := fmt.Fprintf(&extended, eventTmpl, t.Name, t.Name, t.Name, t.Name) |
| if err != nil { |
| return "", err |
| } |
| } |
| return extended.String(), nil |
| } |
| |
| func findDefinitionWithName(name string, s *ast.Schema) *ast.Definition { |
| for n, d := range s.Types { |
| if n == name { |
| return d |
| } |
| } |
| panic(fmt.Sprintf("Expected event input definiton for %s", name)) |
| } |
| |
| func findEventInputDefinitionFor(d *ast.Definition, s *ast.Schema) *ast.Definition { |
| if !strings.HasSuffix(d.Name, "Input") { |
| panic(fmt.Sprintf("Expected input definiton, got %s", d.Name)) |
| } |
| eventInput := fmt.Sprintf("%sEventInput", strings.TrimSuffix(d.Name, "Input")) |
| return findDefinitionWithName(eventInput, s) |
| } |
| |
| func newEventStateValue(s *ast.Schema) *ast.ChildValue { |
| return &ast.ChildValue{ |
| Name: "state", |
| Position: nil, |
| Value: &ast.Value{ |
| Raw: "NEW", |
| Children: ast.ChildValueList{}, |
| Kind: ast.EnumValue, |
| Position: nil, |
| Definition: findDefinitionWithName("EventState", s), |
| VariableDefinition: nil, |
| ExpectedType: nil, |
| }, |
| } |
| } |
| |
| func newEventListValue(d *ast.Definition, s *ast.Schema) *ast.ChildValue { |
| return &ast.ChildValue{ |
| Name: "events", |
| Position: nil, |
| Value: &ast.Value{ |
| Raw: "", |
| Children: ast.ChildValueList{newEventValue(d, s)}, |
| Kind: ast.ListValue, |
| Position: nil, |
| Definition: findEventInputDefinitionFor(d, s), |
| VariableDefinition: nil, |
| ExpectedType: nil, |
| }, |
| } |
| } |
| |
| func newEventValue(d *ast.Definition, s *ast.Schema) *ast.ChildValue { |
| return &ast.ChildValue{ |
| Name: "events", |
| Position: nil, |
| Value: &ast.Value{ |
| Raw: "", |
| Children: ast.ChildValueList{newEventStateValue(s)}, |
| Kind: ast.ObjectValue, |
| Position: nil, |
| Definition: findEventInputDefinitionFor(d, s), |
| VariableDefinition: nil, |
| ExpectedType: nil, |
| }, |
| } |
| } |
| |
| func rewriteValue(v *ast.Value, s *ast.Schema) { |
| if v == nil { |
| panic("Received nil value") |
| } |
| switch v.Kind { |
| case ast.Variable: |
| case ast.IntValue: |
| case ast.FloatValue: |
| case ast.StringValue: |
| case ast.BlockValue: |
| case ast.BooleanValue: |
| case ast.NullValue: |
| case ast.EnumValue: |
| case ast.ListValue: |
| for _, c := range v.Children { |
| rewriteValue(c.Value, s) |
| } |
| case ast.ObjectValue: |
| for _, c := range v.Children { |
| rewriteValue(c.Value, s) |
| } |
| // TODO(giolekva): explicitly get input argument and rewrite only that part. |
| if v.Definition.Kind == ast.InputObject && |
| !strings.HasSuffix(v.Definition.Name, "Event") && |
| !strings.HasSuffix(v.Definition.Name, "EventInput") && |
| !strings.HasSuffix(v.Definition.Name, "Ref") && |
| !strings.HasSuffix(v.Definition.Name, "Filter") && |
| !strings.HasSuffix(v.Definition.Name, "Patch") { |
| v.Children = append(v.Children, newEventListValue(v.Definition, s)) |
| } |
| } |
| } |
| |
| func rewriteQuery(q *ast.QueryDocument, s *ast.Schema) *ast.QueryDocument { |
| for _, op := range q.Operations { |
| if op.Operation != ast.Mutation { |
| continue |
| } |
| for _, sel := range op.SelectionSet { |
| field, ok := sel.(*ast.Field) |
| if !ok { |
| panic(sel) |
| } |
| for _, arg := range field.Arguments { |
| rewriteValue(arg.Value, s) |
| } |
| } |
| } |
| return q |
| |
| } |
| |
| // TODO(giolekva): will be safer to use directive instead |
| func shouldIgnoreDefinition(d *ast.Definition) bool { |
| return d.Kind != ast.Object || |
| d.Name == "Query" || |
| d.Name == "Mutation" || |
| strings.HasPrefix(d.Name, "__") || |
| strings.HasSuffix(d.Name, "Payload") || |
| strings.HasSuffix(d.Name, "Event") |
| } |
| |
| func fixWhitespaces(schema string) string { |
| return strings.ReplaceAll( |
| strings.ReplaceAll(schema, "\n", " "), "\t", " ") |
| } |
| |
| func escapeQuery(query string) string { |
| return strings.ReplaceAll(query, "\"", "\\\"") |
| } |