| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 1 | package schema |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 5 | "errors" |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 6 | "fmt" |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 7 | "io" |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 8 | "io/ioutil" |
| 9 | "net/http" |
| 10 | "strings" |
| 11 | |
| 12 | "github.com/golang/glog" |
| 13 | "github.com/itaysk/regogo" |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 14 | "github.com/vektah/gqlparser" |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 15 | "github.com/vektah/gqlparser/ast" |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 16 | "github.com/vektah/gqlparser/formatter" |
| 17 | //"github.com/bradleyjkemp/memviz" |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 18 | ) |
| 19 | |
| 20 | const jsonContentType = "application/json" |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 21 | const textContentType = "text/plain" |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 22 | |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 23 | // TODO(giolekva): escape |
| 24 | const getSchemaQuery = `{ getGQLSchema() { schema generatedSchema } }` |
| 25 | const setSchemaQuery = `mutation { updateGQLSchema(input: { set: { schema: "%s" } }) { gqlSchema { schema generatedSchema } } }` |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 26 | const runQuery = `{ "query": "%s" }` |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 27 | const eventTmpl = ` |
| 28 | type %sEvent { |
| 29 | id: ID! |
| giolekva | d261825 | 2020-05-02 19:39:02 +0400 | [diff] [blame] | 30 | state: EventState! @search(by: [exact]) |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 31 | node: %s! @hasInverse(field: events) |
| 32 | } |
| 33 | |
| 34 | extend type %s { |
| 35 | events: [%sEvent] @hasInverse(field: node) |
| 36 | }` |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 37 | |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 38 | type DgraphClient struct { |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 39 | gqlAddress string |
| 40 | schemaAddress string |
| 41 | userSchema string |
| 42 | generatedSchema string |
| 43 | schema *ast.Schema |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 44 | } |
| 45 | |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 46 | func NewDgraphClient(gqlAddress, schemaAddress string) (GraphQLClient, error) { |
| 47 | ret := &DgraphClient{ |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 48 | gqlAddress: gqlAddress, |
| 49 | schemaAddress: schemaAddress, |
| 50 | userSchema: "", |
| 51 | generatedSchema: ""} |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 52 | if err := ret.fetchSchema(); err != nil { |
| 53 | return nil, err |
| 54 | } |
| 55 | return ret, nil |
| 56 | } |
| 57 | |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 58 | func (s *DgraphClient) Schema() *ast.Schema { |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 59 | return s.schema |
| 60 | } |
| 61 | |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 62 | func (s *DgraphClient) AddSchema(gqlSchema string) error { |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 63 | extendedSchema, err := s.extendSchema(gqlSchema) |
| 64 | if err != nil { |
| 65 | return err |
| 66 | } else { |
| 67 | return s.SetSchema(extendedSchema) |
| 68 | } |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 69 | } |
| 70 | |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 71 | func (s *DgraphClient) SetSchema(gqlSchema string) error { |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 72 | glog.Info("Setting GraphQL schema") |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 73 | glog.Info(gqlSchema) |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 74 | resp, err := s.runQuery( |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 75 | fmt.Sprintf(setSchemaQuery, gqlSchema), |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 76 | s.schemaAddress) |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 77 | if err != nil { |
| 78 | return err |
| 79 | } |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 80 | data, err := regogo.Get(resp, "input.updateGQLSchema.gqlSchema") |
| 81 | if err != nil { |
| 82 | return err |
| 83 | } |
| 84 | return s.updateSchema(data.JSON()) |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 85 | } |
| 86 | |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 87 | func (s *DgraphClient) fetchSchema() error { |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 88 | glog.Infof("Getting GraphQL schema") |
| 89 | resp, err := s.runQuery(getSchemaQuery, s.schemaAddress) |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 90 | if err != nil { |
| 91 | return err |
| 92 | } |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 93 | data, err := regogo.Get(resp, "input.getGQLSchema") |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 94 | if err != nil { |
| 95 | return err |
| 96 | } |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 97 | return s.updateSchema(data.JSON()) |
| 98 | } |
| 99 | |
| 100 | func (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") |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 106 | if err != nil { |
| 107 | return err |
| 108 | } |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 109 | schema, gqlErr := gqlparser.LoadSchema(&ast.Source{Input: generatedSchema.String()}) |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 110 | if gqlErr != nil { |
| 111 | return gqlErr |
| 112 | } |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 113 | s.userSchema = userSchema.String() |
| 114 | s.generatedSchema = generatedSchema.String() |
| giolekva | c76b21b | 2020-04-18 19:28:43 +0400 | [diff] [blame] | 115 | s.schema = schema |
| 116 | return nil |
| 117 | } |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 118 | |
| 119 | func (s *DgraphClient) RunQuery(query string) (string, error) { |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 120 | q, gqlErr := gqlparser.LoadQuery(s.Schema(), query) |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 121 | if gqlErr != nil { |
| 122 | return "", errors.New(gqlErr.Error()) |
| 123 | } |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 124 | 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() |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 129 | return s.runQuery(query, s.gqlAddress) |
| 130 | } |
| 131 | |
| 132 | func (s *DgraphClient) runQuery(query string, onAddr string) (string, error) { |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 133 | glog.Infof("Running GraphQL query: %s", query) |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 134 | queryJson := fmt.Sprintf(runQuery, fixWhitespaces(escapeQuery(query))) |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 135 | resp, err := http.Post( |
| giolekva | a374a58 | 2020-04-30 11:43:13 +0400 | [diff] [blame] | 136 | onAddr, |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 137 | 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 | |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 159 | func (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 | } |
| giolekva | fe0765f | 2020-05-12 14:09:09 +0400 | [diff] [blame] | 178 | if _, ok := parsed.Types[t.Name+"Event"]; ok { |
| 179 | continue |
| 180 | } |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 181 | _, 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 | |
| 189 | func 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 | |
| giolekva | e7106be | 2020-05-05 18:22:21 +0400 | [diff] [blame] | 198 | func findEventInputDefinitionFor(d *ast.Definition, s *ast.Schema) *ast.Definition { |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 199 | 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 | |
| 206 | func 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 | |
| 222 | func 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, |
| giolekva | e7106be | 2020-05-05 18:22:21 +0400 | [diff] [blame] | 231 | Definition: findEventInputDefinitionFor(d, s), |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 232 | VariableDefinition: nil, |
| 233 | ExpectedType: nil, |
| 234 | }, |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | func 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, |
| giolekva | e7106be | 2020-05-05 18:22:21 +0400 | [diff] [blame] | 247 | Definition: findEventInputDefinitionFor(d, s), |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 248 | VariableDefinition: nil, |
| 249 | ExpectedType: nil, |
| 250 | }, |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | func 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 | } |
| giolekva | ede6d2b | 2020-05-05 22:14:16 +0400 | [diff] [blame] | 275 | // TODO(giolekva): explicitly get input argument and rewrite only that part. |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 276 | if v.Definition.Kind == ast.InputObject && |
| giolekva | e7106be | 2020-05-05 18:22:21 +0400 | [diff] [blame] | 277 | !strings.HasSuffix(v.Definition.Name, "Event") && |
| giolekva | ede6d2b | 2020-05-05 22:14:16 +0400 | [diff] [blame] | 278 | !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") { |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 282 | v.Children = append(v.Children, newEventListValue(v.Definition, s)) |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | func 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 |
| 307 | func 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 | |
| 316 | func fixWhitespaces(schema string) string { |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 317 | return strings.ReplaceAll( |
| 318 | strings.ReplaceAll(schema, "\n", " "), "\t", " ") |
| 319 | } |
| 320 | |
| giolekva | 26a8b5f | 2020-05-01 20:01:13 +0400 | [diff] [blame] | 321 | func escapeQuery(query string) string { |
| giolekva | fb52e0d | 2020-04-23 22:52:13 +0400 | [diff] [blame] | 322 | return strings.ReplaceAll(query, "\"", "\\\"") |
| 323 | } |