blob: 941a16ba97a3b484724f069a3330a409ac8b7087 [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!
30 state: EventState!
31 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 }
178 _, err := fmt.Fprintf(&extended, eventTmpl, t.Name, t.Name, t.Name, t.Name)
179 if err != nil {
180 return "", err
181 }
182 }
183 return extended.String(), nil
184}
185
186func findDefinitionWithName(name string, s *ast.Schema) *ast.Definition {
187 for n, d := range s.Types {
188 if n == name {
189 return d
190 }
191 }
192 panic(fmt.Sprintf("Expected event input definiton for %s", name))
193}
194
195func findEventnputDefinitionFor(d *ast.Definition, s *ast.Schema) *ast.Definition {
196 if !strings.HasSuffix(d.Name, "Input") {
197 panic(fmt.Sprintf("Expected input definiton, got %s", d.Name))
198 }
199 eventInput := fmt.Sprintf("%sEventInput", strings.TrimSuffix(d.Name, "Input"))
200 return findDefinitionWithName(eventInput, s)
201}
202
203func newEventStateValue(s *ast.Schema) *ast.ChildValue {
204 return &ast.ChildValue{
205 Name: "state",
206 Position: nil,
207 Value: &ast.Value{
208 Raw: "NEW",
209 Children: ast.ChildValueList{},
210 Kind: ast.EnumValue,
211 Position: nil,
212 Definition: findDefinitionWithName("EventState", s),
213 VariableDefinition: nil,
214 ExpectedType: nil,
215 },
216 }
217}
218
219func newEventListValue(d *ast.Definition, s *ast.Schema) *ast.ChildValue {
220 return &ast.ChildValue{
221 Name: "events",
222 Position: nil,
223 Value: &ast.Value{
224 Raw: "",
225 Children: ast.ChildValueList{newEventValue(d, s)},
226 Kind: ast.ListValue,
227 Position: nil,
228 Definition: findEventnputDefinitionFor(d, s),
229 VariableDefinition: nil,
230 ExpectedType: nil,
231 },
232 }
233}
234
235func newEventValue(d *ast.Definition, s *ast.Schema) *ast.ChildValue {
236 return &ast.ChildValue{
237 Name: "events",
238 Position: nil,
239 Value: &ast.Value{
240 Raw: "",
241 Children: ast.ChildValueList{newEventStateValue(s)},
242 Kind: ast.ObjectValue,
243 Position: nil,
244 Definition: findEventnputDefinitionFor(d, s),
245 VariableDefinition: nil,
246 ExpectedType: nil,
247 },
248 }
249}
250
251func rewriteValue(v *ast.Value, s *ast.Schema) {
252 if v == nil {
253 panic("Received nil value")
254 }
255 switch v.Kind {
256 case ast.Variable:
257 case ast.IntValue:
258 case ast.FloatValue:
259 case ast.StringValue:
260 case ast.BlockValue:
261 case ast.BooleanValue:
262 case ast.NullValue:
263 case ast.EnumValue:
264 case ast.ListValue:
265 for _, c := range v.Children {
266 rewriteValue(c.Value, s)
267 }
268 case ast.ObjectValue:
269 for _, c := range v.Children {
270 rewriteValue(c.Value, s)
271 }
272 if v.Definition.Kind == ast.InputObject &&
273 !strings.HasSuffix(v.Definition.Name, "Event") {
274 v.Children = append(v.Children, newEventListValue(v.Definition, s))
275 }
276 }
277}
278
279func rewriteQuery(q *ast.QueryDocument, s *ast.Schema) *ast.QueryDocument {
280 for _, op := range q.Operations {
281 if op.Operation != ast.Mutation {
282 continue
283 }
284 for _, sel := range op.SelectionSet {
285 field, ok := sel.(*ast.Field)
286 if !ok {
287 panic(sel)
288 }
289 for _, arg := range field.Arguments {
290 rewriteValue(arg.Value, s)
291 }
292 }
293 }
294 return q
295
296}
297
298// TODO(giolekva): will be safer to use directive instead
299func shouldIgnoreDefinition(d *ast.Definition) bool {
300 return d.Kind != ast.Object ||
301 d.Name == "Query" ||
302 d.Name == "Mutation" ||
303 strings.HasPrefix(d.Name, "__") ||
304 strings.HasSuffix(d.Name, "Payload") ||
305 strings.HasSuffix(d.Name, "Event")
306}
307
308func fixWhitespaces(schema string) string {
giolekvafb52e0d2020-04-23 22:52:13 +0400309 return strings.ReplaceAll(
310 strings.ReplaceAll(schema, "\n", " "), "\t", " ")
311}
312
giolekva26a8b5f2020-05-01 20:01:13 +0400313func escapeQuery(query string) string {
giolekvafb52e0d2020-04-23 22:52:13 +0400314 return strings.ReplaceAll(query, "\"", "\\\"")
315}