github.com/dgraph-io/dgraph@v1.2.8/graphql/resolve/mutation_rewriter.go (about) 1 /* 2 * Copyright 2019 Dgraph Labs, Inc. and Contributors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package resolve 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "strconv" 23 "strings" 24 25 dgoapi "github.com/dgraph-io/dgo/v2/protos/api" 26 "github.com/dgraph-io/dgraph/gql" 27 "github.com/dgraph-io/dgraph/graphql/schema" 28 "github.com/dgraph-io/dgraph/x" 29 "github.com/pkg/errors" 30 ) 31 32 const ( 33 mutationQueryVar = "x" 34 deleteUIDVarMutation = `{ "uid": "uid(x)" }` 35 updateMutationCondition = `gt(len(x), 0)` 36 ) 37 38 type addRewriter struct { 39 frags [][]*mutationFragment 40 } 41 type updateRewriter struct { 42 setFrags []*mutationFragment 43 delFrags []*mutationFragment 44 } 45 type deleteRewriter struct{} 46 47 // A mutationFragment is a partially built Dgraph mutation. Given a GraphQL 48 // mutation input, we traverse the input data and build a Dgraph mutation. That 49 // mutation might require queries (e.g. to check types), conditions (to guard the 50 // upsert mutation to only run in the right conditions), post mutation checks ( 51 // so we can investigate the mutation result and know what guarded mutations 52 // actually ran. 53 // 54 // In the case of XIDs a mutation might result in two fragments - one for the case 55 // of add a new object for the XID and another for link to an existing XID, depending 56 // on what condition evaluates to true in the upsert. 57 type mutationFragment struct { 58 queries []*gql.GraphQuery 59 conditions []string 60 fragment interface{} 61 deletes []interface{} // TODO: functionality for next PR 62 check resultChecker 63 err error 64 } 65 66 // A mutationBuilder can build a json mutation []byte from a mutationFragment 67 type mutationBuilder func(frag *mutationFragment) ([]byte, error) 68 69 // A resultChecker checks an upsert (query) result and returns an error if the 70 // result indicates that the upsert didn't succeed. 71 type resultChecker func(map[string]interface{}) error 72 73 type counter int 74 75 func (c *counter) next() int { 76 *c++ 77 return int(*c) 78 } 79 80 // NewAddRewriter returns new MutationRewriter for add & update mutations. 81 func NewAddRewriter() MutationRewriter { 82 return &addRewriter{} 83 } 84 85 // NewUpdateRewriter returns new MutationRewriter for add & update mutations. 86 func NewUpdateRewriter() MutationRewriter { 87 return &updateRewriter{} 88 } 89 90 // NewDeleteRewriter returns new MutationRewriter for delete mutations.. 91 func NewDeleteRewriter() MutationRewriter { 92 return &deleteRewriter{} 93 } 94 95 // Rewrite takes a GraphQL schema.Mutation add and builds a Dgraph upsert mutation. 96 // m must have a single argument called 'input' that carries the mutation data. 97 // 98 // That argument could have been passed in the mutation like: 99 // 100 // addPost(input: { title: "...", ... }) 101 // 102 // or be passed in a GraphQL variable like: 103 // 104 // addPost(input: $newPost) 105 // 106 // Either way, the data needs to have type information added and have some rewriting 107 // done - for example, rewriting field names from the GraphQL view to what's stored 108 // in Dgraph, and rewriting ID fields from their names to uid. 109 // 110 // For example, a GraphQL add mutation to add an object of type Author, 111 // with GraphQL input object (where country code is @id) : 112 // 113 // { 114 // name: "A.N. Author", 115 // country: { code: "ind", name: "India" }, 116 // posts: [ { title: "A Post", text: "Some text" }] 117 // friends: [ { id: "0x123" } ] 118 // } 119 // 120 // becomes a guarded upsert with two possible paths - one if "ind" already exists 121 // and the other if we create "ind" as part of the mutation. 122 // 123 // Query: 124 // query { 125 // Author4 as Author4(func: uid(0x123)) @filter(type(Author)) { 126 // uid 127 // } 128 // Country2 as Country2(func: eq(Country.code, "ind")) @filter(type(Country)) { 129 // uid 130 // } 131 // } 132 // 133 // And two conditional mutations. Both create a new post and check that the linked 134 // friend is an Author. One links to India if it exists, the other creates it 135 // 136 // "@if(eq(len(Country2), 0) AND eq(len(Author4), 1))" 137 // { 138 // "uid":"_:Author1" 139 // "dgraph.type":["Author"], 140 // "Author.name":"A.N. Author", 141 // "Author.country":{ 142 // "uid":"_:Country2", 143 // "dgraph.type":["Country"], 144 // "Country.code":"ind", 145 // "Country.name":"India" 146 // }, 147 // "Author.posts": [ { 148 // "uid":"_:Post3" 149 // "dgraph.type":["Post"], 150 // "Post.text":"Some text", 151 // "Post.title":"A Post", 152 // } ], 153 // "Author.friends":[ {"uid":"0x123"} ], 154 // } 155 // 156 // and @if(eq(len(Country2), 1) AND eq(len(Author4), 1)) 157 // { 158 // "uid":"_:Author1", 159 // "dgraph.type":["Author"], 160 // "Author.name":"A.N. Author", 161 // "Author.country": { 162 // "uid":"uid(Country2)" 163 // }, 164 // "Author.posts": [ { 165 // "uid":"_:Post3" 166 // "dgraph.type":["Post"], 167 // "Post.text":"Some text", 168 // "Post.title":"A Post", 169 // } ], 170 // "Author.friends":[ {"uid":"0x123"} ], 171 // } 172 func (mrw *addRewriter) Rewrite( 173 m schema.Mutation) (*gql.GraphQuery, []*dgoapi.Mutation, error) { 174 175 mutatedType := m.MutatedType() 176 177 if m.IsArgListType(schema.InputArgName) { 178 return mrw.handleMultipleMutations(m) 179 } 180 181 val := m.ArgValue(schema.InputArgName).(map[string]interface{}) 182 counter := counter(0) 183 mrw.frags = [][]*mutationFragment{rewriteObject(mutatedType, nil, "", &counter, val)} 184 mutations, err := mutationsFromFragments( 185 mrw.frags[0], 186 func(frag *mutationFragment) ([]byte, error) { 187 return json.Marshal(frag.fragment) 188 }, 189 func(frag *mutationFragment) ([]byte, error) { 190 if len(frag.deletes) > 0 { 191 return json.Marshal(frag.deletes) 192 } 193 return nil, nil 194 }) 195 196 return queryFromFragments(mrw.frags[0]), 197 mutations, 198 schema.GQLWrapf(err, "failed to rewrite mutation payload") 199 } 200 201 func (mrw *addRewriter) handleMultipleMutations( 202 m schema.Mutation) (*gql.GraphQuery, []*dgoapi.Mutation, error) { 203 mutatedType := m.MutatedType() 204 val, _ := m.ArgValue(schema.InputArgName).([]interface{}) 205 206 counter := counter(0) 207 var errs error 208 var mutationsAll []*dgoapi.Mutation 209 queries := &gql.GraphQuery{} 210 211 for _, i := range val { 212 obj := i.(map[string]interface{}) 213 frag := rewriteObject(mutatedType, nil, "", &counter, obj) 214 mrw.frags = append(mrw.frags, frag) 215 216 mutations, err := mutationsFromFragments( 217 frag, 218 func(frag *mutationFragment) ([]byte, error) { 219 return json.Marshal(frag.fragment) 220 }, 221 func(frag *mutationFragment) ([]byte, error) { 222 if len(frag.deletes) > 0 { 223 return json.Marshal(frag.deletes) 224 } 225 return nil, nil 226 }) 227 228 errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, 229 "failed to rewrite mutation payload")) 230 231 mutationsAll = append(mutationsAll, mutations...) 232 qry := queryFromFragments(frag) 233 if qry != nil { 234 queries.Children = append(queries.Children, qry.Children...) 235 } 236 } 237 238 if len(queries.Children) == 0 { 239 queries = nil 240 } 241 242 return queries, mutationsAll, errs 243 } 244 245 // FromMutationResult rewrites the query part of a GraphQL add mutation into a Dgraph query. 246 func (mrw *addRewriter) FromMutationResult( 247 mutation schema.Mutation, 248 assigned map[string]string, 249 result map[string]interface{}) (*gql.GraphQuery, error) { 250 251 var errs error 252 253 uids := make([]uint64, 0) 254 255 for _, frag := range mrw.frags { 256 err := checkResult(frag, result) 257 errs = schema.AppendGQLErrs(errs, err) 258 if err != nil { 259 continue 260 } 261 262 node := strings.TrimPrefix(frag[0]. 263 fragment.(map[string]interface{})["uid"].(string), "_:") 264 val, ok := assigned[node] 265 if !ok { 266 continue 267 } 268 uid, err := strconv.ParseUint(val, 0, 64) 269 if err != nil { 270 errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, 271 "received %s as an assigned uid from Dgraph,"+ 272 " but couldn't parse it as uint64", 273 assigned[node])) 274 } 275 276 uids = append(uids, uid) 277 } 278 279 if len(assigned) == 0 && errs == nil { 280 errs = schema.AsGQLErrors(fmt.Errorf("No new node was created")) 281 } 282 283 return rewriteAsQueryByIds(mutation.QueryField(), uids), errs 284 } 285 286 // Rewrite rewrites set and remove update patches into GraphQL+- upsert mutations. 287 // The GraphQL updates look like: 288 // 289 // input UpdateAuthorInput { 290 // filter: AuthorFilter! 291 // set: PatchAuthor 292 // remove: PatchAuthor 293 // } 294 // 295 // which gets rewritten in to a Dgraph upsert mutation 296 // - filter becomes the query 297 // - set becomes the Dgraph set mutation 298 // - remove becomes the Dgraph delete mutation 299 // 300 // The semantics is the same as the Dgraph mutation semantics. 301 // - Any values in set become the new values for those predicates (or add to the existing 302 // values for lists) 303 // - Any nulls in set are ignored. 304 // - Explicit values in remove mean delete this if it is the actual value 305 // - Nulls in remove become like delete * for the corresponding predicate. 306 // 307 // See addRewriter for how the set and remove fragments get created. 308 func (urw *updateRewriter) Rewrite( 309 m schema.Mutation) (*gql.GraphQuery, []*dgoapi.Mutation, error) { 310 311 mutatedType := m.MutatedType() 312 313 inp := m.ArgValue(schema.InputArgName).(map[string]interface{}) 314 setArg := inp["set"] 315 delArg := inp["remove"] 316 317 if setArg == nil && delArg == nil { 318 return nil, nil, nil 319 } 320 321 upsertQuery := rewriteUpsertQueryFromMutation(m) 322 srcUID := fmt.Sprintf("uid(%s)", mutationQueryVar) 323 324 var errSet, errDel error 325 var mutSet, mutDel []*dgoapi.Mutation 326 counter := counter(0) 327 328 if setArg != nil { 329 urw.setFrags = 330 rewriteObject(mutatedType, nil, srcUID, &counter, setArg.(map[string]interface{})) 331 addUpdateCondition(urw.setFrags) 332 mutSet, errSet = mutationsFromFragments( 333 urw.setFrags, 334 func(frag *mutationFragment) ([]byte, error) { 335 return json.Marshal(frag.fragment) 336 }, 337 func(frag *mutationFragment) ([]byte, error) { 338 if len(frag.deletes) > 0 { 339 return json.Marshal(frag.deletes) 340 } 341 return nil, nil 342 }) 343 } 344 345 if delArg != nil { 346 urw.delFrags = 347 rewriteObject(mutatedType, nil, srcUID, &counter, delArg.(map[string]interface{})) 348 addUpdateCondition(urw.delFrags) 349 mutDel, errDel = mutationsFromFragments( 350 urw.delFrags, 351 func(frag *mutationFragment) ([]byte, error) { 352 return nil, nil 353 }, 354 func(frag *mutationFragment) ([]byte, error) { 355 return json.Marshal(frag.fragment) 356 }) 357 } 358 359 queries := []*gql.GraphQuery{upsertQuery} 360 361 q1 := queryFromFragments(urw.setFrags) 362 if q1 != nil { 363 queries = append(queries, q1.Children...) 364 } 365 366 q2 := queryFromFragments(urw.delFrags) 367 if q2 != nil { 368 queries = append(queries, q2.Children...) 369 } 370 371 return &gql.GraphQuery{Children: queries}, 372 append(mutSet, mutDel...), 373 schema.GQLWrapf(schema.AppendGQLErrs(errSet, errDel), "failed to rewrite mutation payload") 374 } 375 376 // FromMutationResult rewrites the query part of a GraphQL update mutation into a Dgraph query. 377 func (urw *updateRewriter) FromMutationResult( 378 mutation schema.Mutation, 379 assigned map[string]string, 380 result map[string]interface{}) (*gql.GraphQuery, error) { 381 382 err := checkResult(urw.setFrags, result) 383 if err != nil { 384 return nil, err 385 } 386 err = checkResult(urw.delFrags, result) 387 if err != nil { 388 return nil, err 389 } 390 391 mutated := extractMutated(result, mutation.ResponseName()) 392 393 var uids []uint64 394 if len(mutated) > 0 { 395 // This is the case of a conditional upsert where we should get uids from mutated. 396 for _, id := range mutated { 397 uid, err := strconv.ParseUint(id, 0, 64) 398 if err != nil { 399 return nil, schema.GQLWrapf(err, 400 "received %s as an updated uid from Dgraph, but couldn't parse it as "+ 401 "uint64", id) 402 } 403 uids = append(uids, uid) 404 } 405 } 406 407 return rewriteAsQueryByIds(mutation.QueryField(), uids), nil 408 } 409 410 func extractMutated(result map[string]interface{}, mutatedField string) []string { 411 var mutated []string 412 413 if val, ok := result[mutatedField].([]interface{}); ok { 414 for _, v := range val { 415 if obj, vok := v.(map[string]interface{}); vok { 416 if uid, uok := obj["uid"].(string); uok { 417 mutated = append(mutated, uid) 418 } 419 } 420 } 421 } 422 423 return mutated 424 } 425 426 func addUpdateCondition(frags []*mutationFragment) { 427 for _, frag := range frags { 428 frag.conditions = append(frag.conditions, updateMutationCondition) 429 } 430 } 431 432 // checkResult checks if any mutationFragment in frags was successful in result. 433 // If any one of the frags (which correspond to conditional mutations) succeeded, 434 // then the mutation ran through ok. Otherwise return an error showing why 435 // at least one of the mutations failed. 436 func checkResult(frags []*mutationFragment, result map[string]interface{}) error { 437 if len(frags) == 0 { 438 return nil 439 } 440 441 if result == nil { 442 return nil 443 } 444 445 var err error 446 for _, frag := range frags { 447 err = frag.check(result) 448 if err == nil { 449 return nil 450 } 451 } 452 453 return err 454 } 455 456 func extractFilter(m schema.Mutation) map[string]interface{} { 457 var filter map[string]interface{} 458 mutationType := m.MutationType() 459 if mutationType == schema.UpdateMutation { 460 input, ok := m.ArgValue("input").(map[string]interface{}) 461 if ok { 462 filter, _ = input["filter"].(map[string]interface{}) 463 } 464 } else if mutationType == schema.DeleteMutation { 465 filter, _ = m.ArgValue("filter").(map[string]interface{}) 466 } 467 return filter 468 } 469 470 func rewriteUpsertQueryFromMutation(m schema.Mutation) *gql.GraphQuery { 471 // The query needs to assign the results to a variable, so that the mutation can use them. 472 dgQuery := &gql.GraphQuery{ 473 Var: mutationQueryVar, 474 Attr: m.ResponseName(), 475 } 476 // Add uid child to the upsert query, so that we can get the list of nodes upserted. 477 dgQuery.Children = append(dgQuery.Children, &gql.GraphQuery{ 478 Attr: "uid", 479 }) 480 481 // TODO - Cache this instead of this being a loop to find the IDField. 482 if ids := idFilter(m, m.MutatedType().IDField()); ids != nil { 483 addUIDFunc(dgQuery, ids) 484 } else { 485 addTypeFunc(dgQuery, m.MutatedType().DgraphName()) 486 } 487 488 filter := extractFilter(m) 489 addFilter(dgQuery, m.MutatedType(), filter) 490 return dgQuery 491 } 492 493 func (drw *deleteRewriter) Rewrite(m schema.Mutation) ( 494 *gql.GraphQuery, []*dgoapi.Mutation, error) { 495 if m.MutationType() != schema.DeleteMutation { 496 497 return nil, nil, errors.Errorf( 498 "(internal error) call to build delete mutation for %s mutation type", 499 m.MutationType()) 500 } 501 502 return rewriteUpsertQueryFromMutation(m), 503 []*dgoapi.Mutation{{ 504 DeleteJson: []byte(deleteUIDVarMutation), 505 }}, 506 nil 507 } 508 509 func (drw *deleteRewriter) FromMutationResult( 510 mutation schema.Mutation, 511 assigned map[string]string, 512 result map[string]interface{}) (*gql.GraphQuery, error) { 513 514 // There's no query that follows a delete 515 return nil, nil 516 } 517 518 func asUID(val interface{}) (uint64, error) { 519 if val == nil { 520 return 0, errors.Errorf("ID value was null") 521 } 522 523 id, ok := val.(string) 524 uid, err := strconv.ParseUint(id, 0, 64) 525 526 if !ok || err != nil { 527 return 0, errors.Errorf("ID argument (%s) was not able to be parsed", id) 528 } 529 530 return uid, nil 531 } 532 533 func mutationsFromFragments( 534 frags []*mutationFragment, 535 setBuilder mutationBuilder, 536 delBuilder mutationBuilder) ([]*dgoapi.Mutation, error) { 537 538 mutations := make([]*dgoapi.Mutation, 0, len(frags)) 539 var errs x.GqlErrorList 540 541 for _, frag := range frags { 542 if frag.err != nil { 543 errs = append(errs, schema.AsGQLErrors(frag.err)...) 544 continue 545 } 546 547 var conditions string 548 if len(frag.conditions) > 0 { 549 conditions = fmt.Sprintf("@if(%s)", strings.Join(frag.conditions, " AND ")) 550 } 551 552 set, err := setBuilder(frag) 553 if err != nil { 554 errs = append(errs, schema.AsGQLErrors(err)...) 555 continue 556 } 557 558 del, err := delBuilder(frag) 559 if err != nil { 560 errs = append(errs, schema.AsGQLErrors(err)...) 561 continue 562 } 563 564 mutations = append(mutations, &dgoapi.Mutation{ 565 SetJson: set, 566 DeleteJson: del, 567 Cond: conditions, 568 }) 569 } 570 571 var err error 572 if len(errs) > 0 { 573 err = errs 574 } 575 return mutations, err 576 } 577 578 func queryFromFragments(frags []*mutationFragment) *gql.GraphQuery { 579 qry := &gql.GraphQuery{} 580 for _, frag := range frags { 581 qry.Children = append(qry.Children, frag.queries...) 582 } 583 584 if len(qry.Children) == 0 { 585 return nil 586 } 587 588 return qry 589 } 590 591 // rewriteObject rewrites obj to a list of mutation fragments. See addRewriter.Rewrite 592 // for a description of what those fragments look like. 593 // 594 // GraphQL validation has already ensured that the types of arguments (or variables) 595 // are correct and has ensured that non-nullables are not null. But for deep mutations 596 // that's not quite enough, and we have add some extra checking on the reference 597 // types. 598 // 599 // Currently adds enforce the schema ! restrictions, but updates don't. 600 // e.g. a Post might have `title: String!`` in the schema, but, a Post update could 601 // set that to to null. ATM we allow this and it'll just triggers GraphQL error propagation 602 // when that is in a query result. This is the same case as deletes: e.g. deleting 603 // an author might make the `author: Author!` field of a bunch of Posts invalid. 604 // (That might actually be helpful if you want to run one mutation to remove something 605 // and then another to correct it.) 606 func rewriteObject( 607 typ schema.Type, 608 srcField schema.FieldDefinition, 609 srcUID string, 610 counter *counter, 611 obj map[string]interface{}) []*mutationFragment { 612 613 atTopLevel := srcField == nil 614 topLevelAdd := srcUID == "" 615 616 variable := fmt.Sprintf("%s%v", typ.Name(), counter.next()) 617 618 id := typ.IDField() 619 if id != nil { 620 if idVal, ok := obj[id.Name()]; ok { 621 if idVal != nil { 622 return []*mutationFragment{asIDReference(idVal, srcField, srcUID, variable)} 623 } 624 delete(obj, id.Name()) 625 } 626 } 627 628 var xidFrag *mutationFragment 629 var xidString string 630 xid := typ.XIDField() 631 if xid != nil { 632 if xidVal, ok := obj[xid.Name()]; ok && xidVal != nil { 633 xidString, ok = xidVal.(string) 634 if !ok { 635 errFrag := newFragment(nil) 636 errFrag.err = errors.New("encountered an XID that isn't a string") 637 return []*mutationFragment{errFrag} 638 } 639 } 640 } 641 642 if !atTopLevel { // top level is never a reference - it's adding/updating 643 if xid != nil && xidString != "" { 644 xidFrag = 645 asXIDReference(srcField, srcUID, typ, xid.Name(), xidString, variable) 646 } 647 } 648 649 if !atTopLevel { // top level mutations are fully checked by GraphQL validation 650 exclude := "" 651 if srcField != nil { 652 invType, invField := srcField.Inverse() 653 if invType != nil && invField != nil { 654 exclude = invField.Name() 655 } 656 } 657 if err := typ.EnsureNonNulls(obj, exclude); err != nil { 658 // This object is either an invalid deep mutation or it's an xid reference 659 // and asXIDReference must to apply or it's an error. 660 return invalidObjectFragment(err, xidFrag, variable, xidString) 661 } 662 } 663 664 var newObj map[string]interface{} 665 var myUID string 666 if !atTopLevel || topLevelAdd { 667 newObj = make(map[string]interface{}, len(obj)+3) 668 dgraphTypes := []string{typ.DgraphName()} 669 dgraphTypes = append(dgraphTypes, typ.Interfaces()...) 670 newObj["dgraph.type"] = dgraphTypes 671 myUID = fmt.Sprintf("_:%s", variable) 672 673 addInverseLink(newObj, srcField, srcUID) 674 675 } else { // it's the top level of an update add/remove 676 newObj = make(map[string]interface{}, len(obj)) 677 myUID = srcUID 678 } 679 newObj["uid"] = myUID 680 681 frag := newFragment(newObj) 682 results := []*mutationFragment{frag} 683 684 // if xidString != "", then we are adding with an xid. In which case, we have to ensure 685 // as part of the upsert that the xid doesn't already exist. 686 if xidString != "" { 687 if atTopLevel { 688 // If not at top level, the query is already added by asXIDReference 689 frag.queries = []*gql.GraphQuery{ 690 xidQuery(variable, xidString, xid.Name(), typ), 691 } 692 } 693 frag.conditions = []string{fmt.Sprintf("eq(len(%s), 0)", variable)} 694 frag.check = checkQueryResult(variable, 695 x.GqlErrorf("id %s already exists for type %s", xidString, typ.Name()), 696 nil) 697 } 698 699 for field, val := range obj { 700 var frags []*mutationFragment 701 702 fieldDef := typ.Field(field) 703 fieldName := typ.DgraphPredicate(field) 704 705 switch val := val.(type) { 706 case map[string]interface{}: 707 // This field is another GraphQL object, which could either be linking to an 708 // existing node by it's ID 709 // { "title": "...", "author": { "id": "0x123" } 710 // like here ^^ 711 // or giving the data to create the object as part of a deep mutation 712 // { "title": "...", "author": { "username": "new user", "dob": "...", ... } 713 // like here ^^ 714 frags = rewriteObject(fieldDef.Type(), fieldDef, myUID, counter, val) 715 case []interface{}: 716 // This field is either: 717 // 1) A list of objects: e.g. if the schema said `categories: [Categories]` 718 // Which can be references to existing objects 719 // { "title": "...", "categories": [ { "id": "0x123" }, { "id": "0x321" }, ...] } 720 // like here ^^ ^^ 721 // Or a deep mutation that creates new objects 722 // { "title": "...", "categories": [ { "name": "new category", ... }, ... ] } 723 // like here ^^ ^^ 724 // 2) Or a list of scalars - e.g. if schema said `scores: [Float]` 725 // { "title": "...", "scores": [10.5, 9.3, ... ] 726 // like here ^^ 727 frags = rewriteList(fieldDef.Type(), fieldDef, myUID, counter, val) 728 default: 729 // This field is either: 730 // 1) a scalar value: e.g. 731 // { "title": "My Post", ... } 732 // 2) a JSON null: e.g. 733 // { "text": null, ... } 734 // e.g. to remove the text or 735 // { "friends": null, ... } 736 // to remove all friends 737 frags = []*mutationFragment{newFragment(val)} 738 } 739 740 results = squashFragments(squashIntoObject(fieldName), results, frags) 741 } 742 743 if xidFrag != nil { 744 results = append(results, xidFrag) 745 } 746 747 return results 748 } 749 750 func invalidObjectFragment( 751 err error, 752 xidFrag *mutationFragment, 753 variable, xidString string) []*mutationFragment { 754 755 if xidFrag != nil { 756 xidFrag.check = 757 checkQueryResult(variable, 758 nil, 759 schema.GQLWrapf(err, "xid \"%s\" doesn't exist and input object not well formed", xidString)) 760 761 return []*mutationFragment{xidFrag} 762 } 763 return []*mutationFragment{{err: err}} 764 } 765 766 func checkQueryResult(qry string, yes, no error) resultChecker { 767 return func(m map[string]interface{}) error { 768 if val, exists := m[qry]; exists && val != nil { 769 if data, ok := val.([]interface{}); ok && len(data) > 0 { 770 return yes 771 } 772 } 773 return no 774 } 775 } 776 777 // asIDReference makes a mutation fragment that resolves a reference to the uid in val. There's 778 // a bit of extra mutation to build if the original mutation contains a reference to 779 // another node: e.g it was say adding a Post with: 780 // { "title": "...", "author": { "id": "0x123" }, ... } 781 // and we'd gotten to here ^^ 782 // in rewriteObject with srcField = "author" srcUID = "XYZ" 783 // and the schema says that Post.author and Author.Posts are inverses of each other, then we need 784 // to make sure that inverse link is added/removed. We have to make sure the Dgraph upsert 785 // mutation ends up like: 786 // 787 // query : 788 // Author1 as Author1(func: uid(0x123)) @filter(type(Author)) { uid } 789 // condition : 790 // len(Author1) > 0 791 // mutation : 792 // { "uid": "XYZ", "title": "...", "author": { "id": "0x123", "posts": [ { "uid": "XYZ" } ] }, ... } 793 // asIDReference builds the fragment 794 // { "id": "0x123", "posts": [ { "uid": "XYZ" } ] } 795 func asIDReference( 796 val interface{}, 797 srcField schema.FieldDefinition, 798 srcUID string, 799 variable string) *mutationFragment { 800 801 result := make(map[string]interface{}, 2) 802 frag := newFragment(result) 803 804 uid, err := asUID(val) 805 if err != nil { 806 frag.err = err 807 return frag 808 } 809 810 result["uid"] = val 811 812 addInverseLink(result, srcField, srcUID) 813 814 qry := &gql.GraphQuery{ 815 Var: variable, 816 Attr: variable, 817 UID: []uint64{uid}, 818 Children: []*gql.GraphQuery{{Attr: "uid"}}, 819 } 820 addTypeFilter(qry, srcField.Type()) 821 addUIDFunc(qry, []uint64{uid}) 822 823 frag.queries = []*gql.GraphQuery{qry} 824 frag.conditions = []string{fmt.Sprintf("eq(len(%s), 1)", variable)} 825 frag.check = 826 checkQueryResult(variable, 827 nil, 828 errors.Errorf("ID \"%#x\" isn't a %s", uid, srcField.Type().Name())) 829 830 return frag 831 832 // FIXME: if this is an update we also need to add a query that checks if 833 // an author exists, and add a mutation to remove this post from that author 834 // query(func: uid(XYZ)) { a as author } 835 // +delete mutation 836 // { uid: uid(a), posts: [ uid: "XYZ"] } 837 // this can only occur at top level, not deep 838 // 839 // mutation was 840 // { "title": "...", "author": { "id": "0x123" }, ... } 841 // 842 // we'll build 843 // query XYZ = ... 844 // query is 123 an author 845 // query(func: uid(XYZ)) { a as author(and not 123) } 846 // { "uid": "XYZ", "title": "...", 847 // "author": { "id": "0x123", "posts": [ { "uid": "XYZ" } ] }, ... } 848 // also 849 // delete { uid: uid(a), posts: [ uid: uid(XYZ)] } 850 // 851 // ** only if author is single - other wise it's always adding to existing edges. ** 852 // ** only if update set mutation ** 853 // ** Also in an add that links to an existing node ** 854 // same sort of thing if it's xid, not id 855 // 856 // should go in some sort of deletes list 857 // 858 // Can tell by the type ??? 859 } 860 861 // asXIDReference makes a mutation fragment that resolves a reference to an XID. There's 862 // a bit of extra mutation to build since if the original mutation contains a reference to 863 // another node, e.g it was say adding a Post with: 864 // { "title": "...", "author": { "username": "A-user" }, ... } 865 // and we'd gotten to here ^^ 866 // in rewriteObject with srcField = "author" srcUID = "XYZ" 867 // and the schema says that Post.author and Author.Posts are inverses of each other, then we need 868 // to make sure that inverse link is added/removed. We have to make sure the Dgraph upsert 869 // mutation ends up like: 870 // 871 // query : 872 // Author1 as Author1(func: eq(username, "A-user")) @filter(type(Author)) { uid } 873 // condition : 874 // len(Author1) > 0 875 // mutation : 876 // { "uid": "XYZ", "title": "...", "author": { "id": "uid(Author1)", "posts": ... 877 // where asXIDReference builds the fragment 878 // { "id": "uid(Author1)", "posts": [ { "uid": "XYZ" } ] } 879 func asXIDReference( 880 srcField schema.FieldDefinition, 881 srcUID string, 882 typ schema.Type, 883 xidFieldName, xidString, xidVariable string) *mutationFragment { 884 885 result := make(map[string]interface{}, 2) 886 frag := newFragment(result) 887 888 result["uid"] = fmt.Sprintf("uid(%s)", xidVariable) 889 890 addInverseLink(result, srcField, srcUID) 891 892 frag.queries = []*gql.GraphQuery{xidQuery(xidVariable, xidString, xidFieldName, typ)} 893 frag.conditions = []string{fmt.Sprintf("eq(len(%s), 1)", xidVariable)} 894 frag.check = checkQueryResult(xidVariable, 895 nil, 896 errors.Errorf("ID \"%s\" isn't a %s", xidString, srcField.Type().Name())) 897 898 // FIXME: and remove any existing 899 900 return frag 901 } 902 903 func addInverseLink(obj map[string]interface{}, srcField schema.FieldDefinition, srcUID string) { 904 if srcField != nil { 905 invType, invField := srcField.Inverse() 906 if invType != nil && invField != nil { 907 if invField.Type().ListType() != nil { 908 obj[invType.DgraphPredicate(invField.Name())] = 909 []interface{}{map[string]interface{}{"uid": srcUID}} 910 } else { 911 obj[invType.DgraphPredicate(invField.Name())] = 912 map[string]interface{}{"uid": srcUID} 913 } 914 } 915 } 916 } 917 918 func xidQuery(xidVariable, xidString, xidPredicate string, typ schema.Type) *gql.GraphQuery { 919 qry := &gql.GraphQuery{ 920 Var: xidVariable, 921 Attr: xidVariable, 922 Func: &gql.Function{ 923 Name: "eq", 924 Args: []gql.Arg{ 925 {Value: typ.DgraphPredicate(xidPredicate)}, 926 {Value: maybeQuoteArg("eq", xidString)}, 927 }, 928 }, 929 Children: []*gql.GraphQuery{{Attr: "uid"}}, 930 } 931 addTypeFilter(qry, typ) 932 return qry 933 } 934 935 func rewriteList( 936 typ schema.Type, 937 srcField schema.FieldDefinition, 938 srcUID string, 939 counter *counter, 940 objects []interface{}) []*mutationFragment { 941 942 frags := []*mutationFragment{newFragment(make([]interface{}, 0))} 943 944 for _, obj := range objects { 945 switch obj := obj.(type) { 946 case map[string]interface{}: 947 frags = squashFragments(squashIntoList, frags, 948 rewriteObject(typ, srcField, srcUID, counter, obj)) 949 default: 950 // All objects in the list must be of the same type. GraphQL validation makes sure 951 // of that. So this must be a list of scalar values (lists of lists aren't allowed). 952 return []*mutationFragment{ 953 newFragment(objects), 954 } 955 } 956 } 957 958 return frags 959 } 960 961 func newFragment(f interface{}) *mutationFragment { 962 return &mutationFragment{ 963 fragment: f, 964 check: func(m map[string]interface{}) error { return nil }, 965 } 966 } 967 968 func squashIntoList(list, v interface{}, makeCopy bool) interface{} { 969 if list == nil { 970 return []interface{}{v} 971 } 972 asList := list.([]interface{}) 973 if makeCopy { 974 cpy := make([]interface{}, len(asList), len(asList)+1) 975 copy(cpy, asList) 976 asList = cpy 977 } 978 return append(asList, v) 979 } 980 981 func squashIntoObject(label string) func(interface{}, interface{}, bool) interface{} { 982 return func(object, v interface{}, makeCopy bool) interface{} { 983 asObject := object.(map[string]interface{}) 984 if makeCopy { 985 cpy := make(map[string]interface{}, len(asObject)+1) 986 for k, v := range asObject { 987 cpy[k] = v 988 } 989 asObject = cpy 990 } 991 asObject[label] = v 992 return asObject 993 } 994 } 995 996 // squashFragments takes two lists of mutationFragments and produces a single list 997 // that has all the right fragments squashed into the left. 998 // 999 // In most cases, this is len(left) == 1 and len(right) == 1 and the result is a 1000 // single fragment. For example, if left is what we have built so far for adding a 1001 // new author and to original input contained: 1002 // { 1003 // ... 1004 // country: { id: "0x123" } 1005 // } 1006 // rewriteObject is called on `{ id: "0x123" }` to create a fragment with 1007 // Query: CountryXYZ as CountryXYZ(func: uid(0x123)) @filter(type(Country)) { uid } 1008 // Condition: eq(len(CountryXYZ), 1) 1009 // Fragment: { id: "0x123" } 1010 // In this case, we just need to add `country: { id: "0x123" }`, the query and condition 1011 // to the left fragment and the result is a single fragment. If there are no XIDs 1012 // in the schema, only 1 fragment can ever be generated. We can always tell if the 1013 // mutation means to link to an existing object (because the ID value is present), 1014 // or if the intention is to create a new object (because the ID value isn't there, 1015 // that means it's not known client side), so there's never any need for more than 1016 // one conditional mutation. 1017 // 1018 // However, if there are XIDs, there can be multiple possible mutations. 1019 // For example, if schema has `Type Country { code: String! @id, name: String! ... }` 1020 // and the mutation input is 1021 // { 1022 // ... 1023 // country: { code: "ind", name: "India" } 1024 // } 1025 // we can't tell from the mutation text if this mutation means to link to an existing 1026 // country or if it's a deep add on the XID `code: "ind"`. If the mutation was 1027 // `country: { code: "ind" }`, we'd know it's a link because they didn't supply 1028 // all the ! fields to correctly create a new country, but from 1029 // `country: { code: "ind", name: "India" }` we have to go to the DB to check. 1030 // So rewriteObject called on `{ code: "ind", name: "India" }` produces two fragments 1031 // 1032 // Query: CountryXYZ as CountryXYZ(func: eq(code, "ind")) @filter(type(Country)) { uid } 1033 // 1034 // Fragment1 (if "ind" already exists) 1035 // Cond: eq(len(CountryXYZ), 1) 1036 // Fragment: { uid: uid(CountryXYZ) } 1037 // 1038 // and 1039 // 1040 // Fragment2 (if "ind" doesn't exist) 1041 // Cond eq(len(CountryXYZ), 0) 1042 // Fragment: { uid: uid(CountryXYZ), code: "ind", name: "India" } 1043 // 1044 // Now we have to squash this into what we've already built for the author (left 1045 // mutationFragment). That'll end up as a result with two fragments (two possible 1046 // mutations guarded by conditions on if the country exists), and to do 1047 // that, we'll need to make some copies, e.g., because we'll end up with 1048 // country: { uid: uid(CountryXYZ) } 1049 // in one fragment, and 1050 // country: { uid: uid(CountryXYZ), code: "ind", name: "India" } 1051 // in the other we need to copy what we've already built for the author to represent 1052 // the different mutation payloads. Same goes for the conditions. 1053 func squashFragments( 1054 combiner func(interface{}, interface{}, bool) interface{}, 1055 left, right []*mutationFragment) []*mutationFragment { 1056 1057 if len(left) == 0 { 1058 return right 1059 } 1060 1061 if len(right) == 0 { 1062 return left 1063 } 1064 1065 result := make([]*mutationFragment, 0, len(left)*len(right)) 1066 for _, l := range left { 1067 for _, r := range right { 1068 var conds []string 1069 1070 if len(l.conditions) > 0 { 1071 conds = make([]string, len(l.conditions), len(l.conditions)+len(r.conditions)) 1072 copy(conds, l.conditions) 1073 } 1074 1075 result = append(result, &mutationFragment{ 1076 conditions: append(conds, r.conditions...), 1077 fragment: combiner(l.fragment, r.fragment, len(right) > 1), 1078 check: func(lcheck, rcheck resultChecker) resultChecker { 1079 return func(m map[string]interface{}) error { 1080 return schema.AppendGQLErrs(lcheck(m), rcheck(m)) 1081 } 1082 }(l.check, r.check), 1083 err: schema.AppendGQLErrs(l.err, r.err), 1084 }) 1085 } 1086 } 1087 1088 // queries don't need copying, they just need to be all collected at the end, so 1089 // accumulate them all into one of the result fragments 1090 var queries []*gql.GraphQuery 1091 for _, l := range left { 1092 queries = append(queries, l.queries...) 1093 } 1094 for _, r := range right { 1095 queries = append(queries, r.queries...) 1096 } 1097 result[0].queries = queries 1098 1099 return result 1100 }