blob: f958af6b528bad76c8aa21976433ab318a2c25af [file] [log] [blame]
giolekvac76b21b2020-04-18 19:28:43 +04001package schema
2
3import (
4 "bytes"
giolekvafb52e0d2020-04-23 22:52:13 +04005 "errors"
giolekvac76b21b2020-04-18 19:28:43 +04006 "fmt"
giolekva26a8b5f2020-05-01 20:01:13 +04007 "io"
giolekvac76b21b2020-04-18 19:28:43 +04008 "io/ioutil"
9 "net/http"
10 "strings"
11
12 "github.com/golang/glog"
13 "github.com/itaysk/regogo"
giolekvafb52e0d2020-04-23 22:52:13 +040014 "github.com/vektah/gqlparser"
giolekvac76b21b2020-04-18 19:28:43 +040015 "github.com/vektah/gqlparser/ast"
giolekva26a8b5f2020-05-01 20:01:13 +040016 "github.com/vektah/gqlparser/formatter"
17 //"github.com/bradleyjkemp/memviz"
giolekvac76b21b2020-04-18 19:28:43 +040018)
19
20const jsonContentType = "application/json"
giolekvafb52e0d2020-04-23 22:52:13 +040021const textContentType = "text/plain"
giolekvac76b21b2020-04-18 19:28:43 +040022
giolekvaa374a582020-04-30 11:43:13 +040023// TODO(giolekva): escape
24const getSchemaQuery = `{ getGQLSchema() { schema generatedSchema } }`
25const setSchemaQuery = `mutation { updateGQLSchema(input: { set: { schema: "%s" } }) { gqlSchema { schema generatedSchema } } }`
giolekvafb52e0d2020-04-23 22:52:13 +040026const runQuery = `{ "query": "%s" }`
giolekva26a8b5f2020-05-01 20:01:13 +040027const eventTmpl = `
28type %sEvent {
29 id: ID!
giolekvad2618252020-05-02 19:39:02 +040030 state: EventState! @search(by: [exact])
giolekva26a8b5f2020-05-01 20:01:13 +040031 node: %s! @hasInverse(field: events)
32}
33
34extend type %s {
35 events: [%sEvent] @hasInverse(field: node)
36}`
giolekvac76b21b2020-04-18 19:28:43 +040037
giolekvafb52e0d2020-04-23 22:52:13 +040038type DgraphClient struct {
giolekvaa374a582020-04-30 11:43:13 +040039 gqlAddress string
40 schemaAddress string
41 userSchema string
42 generatedSchema string
43 schema *ast.Schema
giolekvac76b21b2020-04-18 19:28:43 +040044}
45
giolekvafb52e0d2020-04-23 22:52:13 +040046func NewDgraphClient(gqlAddress, schemaAddress string) (GraphQLClient, error) {
47 ret := &DgraphClient{
giolekvaa374a582020-04-30 11:43:13 +040048 gqlAddress: gqlAddress,
49 schemaAddress: schemaAddress,
50 userSchema: "",
51 generatedSchema: ""}
giolekvac76b21b2020-04-18 19:28:43 +040052 if err := ret.fetchSchema(); err != nil {
53 return nil, err
54 }
55 return ret, nil
56}
57
giolekvafb52e0d2020-04-23 22:52:13 +040058func (s *DgraphClient) Schema() *ast.Schema {
giolekvac76b21b2020-04-18 19:28:43 +040059 return s.schema
60}
61
giolekvafb52e0d2020-04-23 22:52:13 +040062func (s *DgraphClient) AddSchema(gqlSchema string) error {
giolekva26a8b5f2020-05-01 20:01:13 +040063 extendedSchema, err := s.extendSchema(gqlSchema)
64 if err != nil {
65 return err
66 } else {
67 return s.SetSchema(extendedSchema)
68 }
giolekvac76b21b2020-04-18 19:28:43 +040069}
70
giolekvafb52e0d2020-04-23 22:52:13 +040071func (s *DgraphClient) SetSchema(gqlSchema string) error {
giolekvaa374a582020-04-30 11:43:13 +040072 glog.Info("Setting GraphQL schema")
giolekvac76b21b2020-04-18 19:28:43 +040073 glog.Info(gqlSchema)
giolekvaa374a582020-04-30 11:43:13 +040074 resp, err := s.runQuery(
giolekva26a8b5f2020-05-01 20:01:13 +040075 fmt.Sprintf(setSchemaQuery, gqlSchema),
giolekvaa374a582020-04-30 11:43:13 +040076 s.schemaAddress)
giolekvac76b21b2020-04-18 19:28:43 +040077 if err != nil {
78 return err
79 }
giolekva26a8b5f2020-05-01 20:01:13 +040080 data, err := regogo.Get(resp, "input.updateGQLSchema.gqlSchema")
81 if err != nil {
82 return err
83 }
84 return s.updateSchema(data.JSON())
giolekvac76b21b2020-04-18 19:28:43 +040085}
86
giolekvafb52e0d2020-04-23 22:52:13 +040087func (s *DgraphClient) fetchSchema() error {
giolekvaa374a582020-04-30 11:43:13 +040088 glog.Infof("Getting GraphQL schema")
89 resp, err := s.runQuery(getSchemaQuery, s.schemaAddress)
giolekvac76b21b2020-04-18 19:28:43 +040090 if err != nil {
91 return err
92 }
giolekva26a8b5f2020-05-01 20:01:13 +040093 data, err := regogo.Get(resp, "input.getGQLSchema")
giolekvac76b21b2020-04-18 19:28:43 +040094 if err != nil {
95 return err
96 }
giolekva26a8b5f2020-05-01 20:01:13 +040097 return s.updateSchema(data.JSON())
98}
99
100func (s *DgraphClient) updateSchema(resp string) error {
101 userSchema, err := regogo.Get(resp, "input.schema")
102 if err != nil {
103 return err
104 }
105 generatedSchema, err := regogo.Get(resp, "input.generatedSchema")
giolekvac76b21b2020-04-18 19:28:43 +0400106 if err != nil {
107 return err
108 }
giolekvaa374a582020-04-30 11:43:13 +0400109 schema, gqlErr := gqlparser.LoadSchema(&ast.Source{Input: generatedSchema.String()})
giolekvac76b21b2020-04-18 19:28:43 +0400110 if gqlErr != nil {
111 return gqlErr
112 }
giolekvaa374a582020-04-30 11:43:13 +0400113 s.userSchema = userSchema.String()
114 s.generatedSchema = generatedSchema.String()
giolekvac76b21b2020-04-18 19:28:43 +0400115 s.schema = schema
116 return nil
117}
giolekvafb52e0d2020-04-23 22:52:13 +0400118
119func (s *DgraphClient) RunQuery(query string) (string, error) {
giolekva26a8b5f2020-05-01 20:01:13 +0400120 q, gqlErr := gqlparser.LoadQuery(s.Schema(), query)
giolekvafb52e0d2020-04-23 22:52:13 +0400121 if gqlErr != nil {
122 return "", errors.New(gqlErr.Error())
123 }
giolekva26a8b5f2020-05-01 20:01:13 +0400124 rewritten := rewriteQuery(q, s.Schema())
125 var b strings.Builder
126 // TODO(giolekva): gqlparser should be reporting error back
127 formatter.NewFormatter(&b).FormatQueryDocument(rewritten)
128 query = b.String()
giolekvaa374a582020-04-30 11:43:13 +0400129 return s.runQuery(query, s.gqlAddress)
130}
131
132func (s *DgraphClient) runQuery(query string, onAddr string) (string, error) {
giolekvafb52e0d2020-04-23 22:52:13 +0400133 glog.Infof("Running GraphQL query: %s", query)
giolekva26a8b5f2020-05-01 20:01:13 +0400134 queryJson := fmt.Sprintf(runQuery, fixWhitespaces(escapeQuery(query)))
giolekvafb52e0d2020-04-23 22:52:13 +0400135 resp, err := http.Post(
giolekvaa374a582020-04-30 11:43:13 +0400136 onAddr,
giolekvafb52e0d2020-04-23 22:52:13 +0400137 jsonContentType,
138 bytes.NewReader([]byte(queryJson)))
139 if err != nil {
140 return "", err
141 }
142 respBody, err := ioutil.ReadAll(resp.Body)
143 if err != nil {
144 return "", err
145 }
146 respStr := string(respBody)
147 glog.Infof("Result: %s", string(respStr))
148 // errStr, err := regogo.Get(respStr, "input.errors")
149 // if err == nil {
150 // return "", errors.New(errStr.JSON())
151 // }
152 data, err := regogo.Get(respStr, "input.data")
153 if err != nil {
154 return "", err
155 }
156 return data.JSON(), nil
157}
158
giolekva26a8b5f2020-05-01 20:01:13 +0400159func (s *DgraphClient) extendSchema(schema string) (string, error) {
160 try := s.generatedSchema + " " + schema
161 parsed, gqlErr := gqlparser.LoadSchema(&ast.Source{Input: try})
162 if gqlErr != nil {
163 return "", errors.New(gqlErr.Error())
164 }
165 var extended strings.Builder
166 _, err := io.WriteString(&extended, s.userSchema)
167 if err != nil {
168 return "", err
169 }
170 _, err = io.WriteString(&extended, schema)
171 if err != nil {
172 return "", err
173 }
174 for _, t := range parsed.Types {
175 if shouldIgnoreDefinition(t) {
176 continue
177 }
giolekvafe0765f2020-05-12 14:09:09 +0400178 if _, ok := parsed.Types[t.Name+"Event"]; ok {
179 continue
180 }
giolekva26a8b5f2020-05-01 20:01:13 +0400181 _, err := fmt.Fprintf(&extended, eventTmpl, t.Name, t.Name, t.Name, t.Name)
182 if err != nil {
183 return "", err
184 }
185 }
186 return extended.String(), nil
187}
188
189func findDefinitionWithName(name string, s *ast.Schema) *ast.Definition {
190 for n, d := range s.Types {
191 if n == name {
192 return d
193 }
194 }
195 panic(fmt.Sprintf("Expected event input definiton for %s", name))
196}
197
giolekvae7106be2020-05-05 18:22:21 +0400198func findEventInputDefinitionFor(d *ast.Definition, s *ast.Schema) *ast.Definition {
giolekva26a8b5f2020-05-01 20:01:13 +0400199 if !strings.HasSuffix(d.Name, "Input") {
200 panic(fmt.Sprintf("Expected input definiton, got %s", d.Name))
201 }
202 eventInput := fmt.Sprintf("%sEventInput", strings.TrimSuffix(d.Name, "Input"))
203 return findDefinitionWithName(eventInput, s)
204}
205
206func newEventStateValue(s *ast.Schema) *ast.ChildValue {
207 return &ast.ChildValue{
208 Name: "state",
209 Position: nil,
210 Value: &ast.Value{
211 Raw: "NEW",
212 Children: ast.ChildValueList{},
213 Kind: ast.EnumValue,
214 Position: nil,
215 Definition: findDefinitionWithName("EventState", s),
216 VariableDefinition: nil,
217 ExpectedType: nil,
218 },
219 }
220}
221
222func newEventListValue(d *ast.Definition, s *ast.Schema) *ast.ChildValue {
223 return &ast.ChildValue{
224 Name: "events",
225 Position: nil,
226 Value: &ast.Value{
227 Raw: "",
228 Children: ast.ChildValueList{newEventValue(d, s)},
229 Kind: ast.ListValue,
230 Position: nil,
giolekvae7106be2020-05-05 18:22:21 +0400231 Definition: findEventInputDefinitionFor(d, s),
giolekva26a8b5f2020-05-01 20:01:13 +0400232 VariableDefinition: nil,
233 ExpectedType: nil,
234 },
235 }
236}
237
238func newEventValue(d *ast.Definition, s *ast.Schema) *ast.ChildValue {
239 return &ast.ChildValue{
240 Name: "events",
241 Position: nil,
242 Value: &ast.Value{
243 Raw: "",
244 Children: ast.ChildValueList{newEventStateValue(s)},
245 Kind: ast.ObjectValue,
246 Position: nil,
giolekvae7106be2020-05-05 18:22:21 +0400247 Definition: findEventInputDefinitionFor(d, s),
giolekva26a8b5f2020-05-01 20:01:13 +0400248 VariableDefinition: nil,
249 ExpectedType: nil,
250 },
251 }
252}
253
254func rewriteValue(v *ast.Value, s *ast.Schema) {
255 if v == nil {
256 panic("Received nil value")
257 }
258 switch v.Kind {
259 case ast.Variable:
260 case ast.IntValue:
261 case ast.FloatValue:
262 case ast.StringValue:
263 case ast.BlockValue:
264 case ast.BooleanValue:
265 case ast.NullValue:
266 case ast.EnumValue:
267 case ast.ListValue:
268 for _, c := range v.Children {
269 rewriteValue(c.Value, s)
270 }
271 case ast.ObjectValue:
272 for _, c := range v.Children {
273 rewriteValue(c.Value, s)
274 }
giolekvaede6d2b2020-05-05 22:14:16 +0400275 // TODO(giolekva): explicitly get input argument and rewrite only that part.
giolekva26a8b5f2020-05-01 20:01:13 +0400276 if v.Definition.Kind == ast.InputObject &&
giolekvae7106be2020-05-05 18:22:21 +0400277 !strings.HasSuffix(v.Definition.Name, "Event") &&
giolekvaede6d2b2020-05-05 22:14:16 +0400278 !strings.HasSuffix(v.Definition.Name, "EventInput") &&
279 !strings.HasSuffix(v.Definition.Name, "Ref") &&
280 !strings.HasSuffix(v.Definition.Name, "Filter") &&
281 !strings.HasSuffix(v.Definition.Name, "Patch") {
giolekva26a8b5f2020-05-01 20:01:13 +0400282 v.Children = append(v.Children, newEventListValue(v.Definition, s))
283 }
284 }
285}
286
287func rewriteQuery(q *ast.QueryDocument, s *ast.Schema) *ast.QueryDocument {
288 for _, op := range q.Operations {
289 if op.Operation != ast.Mutation {
290 continue
291 }
292 for _, sel := range op.SelectionSet {
293 field, ok := sel.(*ast.Field)
294 if !ok {
295 panic(sel)
296 }
297 for _, arg := range field.Arguments {
298 rewriteValue(arg.Value, s)
299 }
300 }
301 }
302 return q
303
304}
305
306// TODO(giolekva): will be safer to use directive instead
307func shouldIgnoreDefinition(d *ast.Definition) bool {
308 return d.Kind != ast.Object ||
309 d.Name == "Query" ||
310 d.Name == "Mutation" ||
311 strings.HasPrefix(d.Name, "__") ||
312 strings.HasSuffix(d.Name, "Payload") ||
313 strings.HasSuffix(d.Name, "Event")
314}
315
316func fixWhitespaces(schema string) string {
giolekvafb52e0d2020-04-23 22:52:13 +0400317 return strings.ReplaceAll(
318 strings.ReplaceAll(schema, "\n", " "), "\t", " ")
319}
320
giolekva26a8b5f2020-05-01 20:01:13 +0400321func escapeQuery(query string) string {
giolekvafb52e0d2020-04-23 22:52:13 +0400322 return strings.ReplaceAll(query, "\"", "\\\"")
323}