Facets in GraphQL

Motivation

Add native support for @facets in GraphQL

User Impact

Users will now be able to mutate and query facets in GraphQL.

Implementation

1. Scalar

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: String
}

The above input GraphQL schema will generate the following types in the output schema:

input AddPersonInput {
    name: String!
    pet: PersonPetFacetRef
}

input PersonPetFacetRef {
    value: String
    petClose: Boolean
    petSince: DateTime
}

type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: PersonPetFacetPayload
}
type PersonPetFacetPayload {
    value: String
    petClose: Boolean
    petSince: DateTime
}

The following GraphQL mutation

mutation {
  addPerson(input: [{
    name: "Carol", pet:{ value:"Jerry",petClose:true, petSince: 2006-01-02T15:04:05}}]) {
    person {
      ...
    }
  }
}

would be rewritten to this DQL mutation

{
  "uid": "_:Carol",
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": "Jerry",
  "Person.pet|since": "2006-01-02T15:04:05",
  "Person.pet|close": true
}

The GraphQL query

{
  queryPerson {
    name
    pet {
      value 
      petClose
      petSince
    }
  }
}

would be rewritten to

{
    queryPerson(func: type(Person)) {
      Person.name
      Person.pet @facets(close,since)
   }
}

Dgraph Response

{
  "data": {
    "queryPerson": [
      {
        "Person.name": "Carol",
        "Person.pet": "Jerry",
        "Person.pet|close": true,
        "Person.pet|since": "2006-01-02T15:04:05"
      }
    ]
  }
}

GraphQL Response

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": {
          "value": "Jerry",
          "petClose": true,
          "petSince": "2006-01-02T15:04:05"
        }
      }
    ]
  }
}

2. UID

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: Animal
}
type Animal {
    id: ID!
    name: String!
    color: String
}

The above input GraphQL schema will generate the following types in the output schema:

input AddPersonInput {
    name: String!
    pet: PersonPetFacetRef
}
input AnimalRef {
    id: ID!
    name: String!
    color: String
}
input PersonPetFacetRef {
    value: AnimalRef
    petClose: Boolean
    petSince: DateTime
}
type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: PersonPetFacetPayload
}
type PersonPetFacetPayload {
    value: Animal
    petClose: Boolean
    petSince: DateTime
}

The following GraphQL mutation

mutation {
  addPerson(input: [{
      name: "Carol", pet:{ value:{name:"Jerry",color:"Gray"},petClose:true,petSince: "2006-01-02T15:04:05"}}]) {
      person {
      ...
    }
  }
}

would be rewritten to this DQL mutation

{
  "uid": "_:Carol",
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": {
    "uid": "_:Jerry",
    "Animal.name": "Jerry",
    "Animal.color": "Gray",
    "Person.pet|close": true,
    "Person.pet|since": "2006-01-02T15:04:05",
    "dgraph.type": "Animal"
  }
}

GraphQL query

{
  queryPerson {
    name
    pet {
      value{
       name
       color
      }
      petClose
      petSince
    }
  }
}

would be rewritten to

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(close,since) {
       Animal.name
       Animal.color
     }
   }
}

Dgraph Response:

{
  "data": {
    "queryPerson": [
      {
        "Person.name": "Carol",
        "Person.pet": {
          "Animal.name": "Jerry",
          "Animal.color": "Gray",
          "Person.pet|close": true,
          "Person.pet|since": "2006-01-02T15:04:05"
        }
      }
    ]
  }
}

GraphQL Response:

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": {
          "value": {
            "name": "Jerry",
            "color": "Gray"
          },
          "petClose": true,
          "petSince": "2006-01-02T15:04:05"
        }
      }
    ]
  }
}

3. [UID]

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: [Animal]
}
type Animal {
    id: ID!
    name: String!
    color: String
}

The above input GraphQL schema will generate the following types in the output schema:

input AddPersonInput {
    name: String!
    pet: [PersonPetFacetRef]
}
input AnimalRef {
    id: ID!
    name: String!
    color: String
}
input PersonPetFacetRef {
    value: AnimalRef
    petClose: Boolean
    petSince: DateTime
}
type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: [PersonPetFacetPayload]
}
type PersonPetFacetPayload {
    value: Animal
    petClose: Boolean
    petSince: DateTime
}

The following GraphQL mutation

mutation {
  addPerson(input: [{
    name: "Carol", pet: [
    {value: {name: "Jerry"}, petClose:true, petSince: "2006-01-02T15:04:05"},
    {value: {name: "Tom", color: "Grey"}, petClose:false, petSince: "2007-01-02T15:04:05"},
    {value: {name: "Donald"}, petClose:false, petSince: "2008-01-02T15:04:05"}, }]}]) {
    numUids
  }
}

would be rewritten to this DQL mutation

{
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": [
    {
      "Animal.name": "Jerry",
      "Person.pet|close": true,
      "Person.pet|since": "2006-01-02T15:04:05",
      "dgraph.type": "Animal"
    },
    {
      "Animal.name": "Tom",
      "Animal.color": "Grey",
      "Person.pet|close": false,
      "Person.pet|since": "2007-01-02T15:04:05",
      "dgraph.type": "Animal"
    },
    {
      "Animal.name": "Donald",
      "Person.friend|close": "false",
      "Person.pet|since": "2008-01-02T15:04:05",
      "dgraph.type": "Animal"
    }
  ]
}

GraphQL query

{
  queryPerson {
    name
    pet {
      value {
          name
          color
      }
      petClose
      petSince
    }
  }
}

would be rewritten to

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(close,since) {
       Animal.name
       Animal.color
     }
   }
}

Dgraph Response:

{
  "data": {
    "queryPerson": [
      {
        "Person.name": "Carol",
        "Person.pet": [
          {
            "Animal.name": "Jerry",
            "Person.pet|close": true,
            "Person.pet|since": "2006-01-02T15:04:05"
          },
          {
            "Animal.name": "Tom",
            "Animal.color": "Grey",
            "Person.pet|close": false,
            "Person.pet|since": "2007-01-02T15:04:05"
          },
          {
            "Animal.name": "Donald",
            "Person.friend|close": "false",
            "Person.pet|since": "2008-01-02T15:04:05"
          }
        ]
      }
    ]
  }
}

GraphQL Response:

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": [
          {
            "value": {
              "name": "Jerry",
              "color": null
            },
            "petClose": true,
            "petSince": "2006-01-02T15:04:05"
          },
          {
            "value": {
              "name": "Tom",
              "color": "Grey"
            },
            "petClose": false,
            "petSince": "2007-01-02T15:04:05"
          },
          {
            "value": {
              "name": "Donald",
              "color": null
            },
            "petClose": false,
            "petSince": "2008-01-02T15:04:05"
          }
        ]
      }
    ]
  }
}

4. [Scalar]

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: [String]
}

The above input GraphQL schema will generate the following types in the output schema:

input AddPersonInput {
    name: String!
    pet: [PersonPetFacetRef]
}

input PersonPetFacetRef {
    value: String
    petClose: Boolean
    petSince: DateTime
}
type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    value: [PersonPetFacetPayload]
}
type PersonPetFacetPayload {
    value: String
    petClose: Boolean
    petSince: DateTime
}

The following GraphQL mutation

mutation {
    addPerson(input: [{
         name: "Carol", pet: [
         { value: "Jerry", petClose: true, petSince: "2006-01-02T15:04:05" },
         { value: "Tom", petSince: "2007-01-02T15:04:05" },
         { value: "Donald", petClose: false }
       ]
     }]) 
    {
       numUids
    }
} 

would be rewritten to this DQL mutation

{
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": [
    "Jerry",
    "Tom",
    "Donald"
  ],
  "Person.pet|close": {
    "0": true,
    "2": false
  },
  "Person.pet|since": {
    "0": "2006-01-02T15:04:05",
    "1": "2007-01-02T15:04:05"
  }
}

The GraphQL query

{
  queryPerson {
    name
    pet {
      value
      petClose
      petSince
    }
  }
}

would be rewritten to

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(close,since)
   }
}

Dgraph Response:

{
  "data": {
    "queryPerson": {
      "Person.name": "Carol",
      "Person.pet": [
        "Jerry",
        "Tom",
        "Donald"
      ],
      "Person.pet|close": {
        "0": true,
        "2": false
      },
      "Person.pet|since": {
        "0": "2006-01-02T15:04:05",
        "1": "2007-01-02T15:04:05"
      }
    }
  }
}

GraphQL Response:

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": [
          {
            "value": "Jerry",
            "petClose": true,
            "petSince": "2006-01-02T15:04:05"
          },
          {
            "value": "Tom",
            "petClose": null,
            "petSince": "2007-01-02T15:04:05"
          },
          {
            "value": "Donald",
            "petClose": false,
            "petSince": null
          }
        ]
      }
    ]
  }
}

Interaction with existing directives

  1. @hasInverse: Can’t be applied on a field with @facet. But, should continue to work the same on any other object field, even on the field which has facets.
  2. @search: Facets don’t require any indexing, so explicitly specifying @search won’t have any effect. Facet fields will have filtering built automatically.
  3. @dgraph: NA
  4. @id: NA.
  5. @withSubscription: NA.
  6. @secret: NA
  7. @auth: NA
  8. @custom: NA
  9. @cascade: Won’t have any effect on facet fields in the query. Should work as expected for other fields.

Open Questions

Mutation

  • While adding/updating, facets would only be rewritten as part of a DQL mutation, if the field on which the facet exist was also added/updated. If only facets are provided for add/update without the corresponding field, then they are not rewritten into the DQL mutation and are dropped. Update case needs to be checked?

Queries

  • If the user requests a facet without the field that it is on, should we handle it or should we return an error?

References

1 Like

Some things to think about while you work on this.

  1. How would facets on scalar list work? Right now a map is returned for a query but maps are not possible in GraphQL, so maybe we return a list for facets field but with null values for the array index where the facet doesn’t exist.

  2. DQL supports filtering on facets without any indexes. How would that work here? We don’t need to support it right away but there should be a way to do it later.

  3. How would sorting by facets work?

1 Like

Filtering:

Scalar

GraphQL query:

{
  queryPerson {
    name
    pet(filter: {petClose: {eq: true}}) {
      value 
      petClose
      petSince
    }
  }
}

rewrites to:

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet  @facets(eq(close,true)) @facets(close,since)
   }
}

[Scalar]

{
  queryPerson {
    name
    pet(filter: {petClose: {eq: true}}){
      value
      petClose
      petSince
    }
  }
}

rewrites to:

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(eq(close,true)) @facets(close,since)
   }
}

UID

{
  queryPerson {
    name
    pet(filter: {petClose: {eq: true}}) {
      value{
       name
       color
      }
      petClose
      petSince
    }
  }
}

rewrites to:

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(eq(close,true)) @facets(close,since) {
       Animal.name
       Animal.color
     }
   }
}

[UID]

{
  queryPerson {
    name
    pet(filter: {petClose: {eq: true}}){
      value {
          name
          color
      }
      petClose
      petSince
    }
  }

rewrites to:

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(eq(close,true)) @facets(since){
       Animal.name
       Animal.color
     }
   }
}

Sorting

Sorting by facets is only possible on UID list.
The below GraphQL query will sort pets of a person by the facet petSince.

{
  queryPerson  {
    name
    pet(order: {asc: petSince}) {
      value {
        name
        color
      }
      petClose
      petSince
    }
  }
}

and it will be written to Dgraph as follows:

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(ordeasc:since, close) {
       Animal.name
       Animal.color
     }
   }
}

Why change away from the norm in GQL filtering? It is normally predicate:{operator:value}

Corrected, It was by mistake.

I think this is really well done and thorough. I’ve just got a question about facets in general and our motivation for wanting to add them to GraphQL.

With this change, I can write:

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: String
}

and then write a mutation like

mutation {
  addPerson(input: [{name: "Carol", pet:{ value:"Jerry",petClose:true, petSince: 2006-01-02T15:04:05}}]) {
    person {
      ...
    }
  }

and then write queries like

{
  queryPerson {
    name
    pet(filter: {petClose: {eq: true}}) {
      value 
      petClose
      petSince
    }
  }
}

My question is, how is this any different to what I can do already? Already I could write something like

type Person {
    name: String!
    pet: PersonPetFacet
}

type PersonPetFacet {
    petClose: Boolean 
    petSince: DateTime 
    value: String
}

And I’d be able to write exactly the same mutation and exactly the same query (though, I’d pick different names for the type and fields) … so what did the @facet gain me ?

I’d like to understand what fundamentally changes with facets, or what users can do with them, that they can’t do without.

I know it’s asked about lots … but, given that it’s a Dgraph thing, not a GraphQL thing, I wonder if it’s asked by folks who have a Dgraph instance and want to understand how to map their existing facets to GraphQL. In which case is it really a problem of ‘facets in GraphQL’ or of supporting existing Dgraph schemas in some nice way.

3 Likes

How would a schema update go?

If I have a schema originally of this:

type Person {
    name: String!
    friend: [Person!]
}

And I realized my name is Mark Zuckerberg, so I gotta be extra creepy, so I need to change my schema to:

type Person {
    name: String!
    friendSince: [DateTime!] @facet(field: "friend", name:"since")
    friend: [Person!] 
}

How would a “migration” be like in your version?

I’m always suspicious of magic schema updates that ‘never’ need data migrations.

Even in yours, you’ve given

friendSince: [DateTime!]

Which means all existing friend relations will be invalid because the don’t have a ‘since’ facet, so that already requires a data migration/insertion.

More broadly, even if there were some magic process with facets, you’d still bump into cases where, for example, you’ve modelled the pet thing with facets and then realise that you need to also record the PetToys that belong with the pet ownership, but you can’t cause you can’t do uid’s in facets, so you have to do a data migration from facets to the model I proposed.

In the end, facets are just a less powerful mechanism of expressing complex relationships than reification. So, for me, facets really just introduces an extra modelling problem (a choice about when to use facets vs reification) rather than adding any expressive power to the modelling language.

2 Likes

For me, facets remove the connecting node in the middle approach and removes a level in a relationship making other filters easier.

I think it is key to note this from the docs:

Though you may find yourself leaning towards facets many times, they should not be misused. It wouldn’t be correct modeling to give the friend edge a facet date_of_birth . That should be an edge for the friend. However, a facet like start_of_friendship might be appropriate. Facets are however not first class citizen in Dgraph like predicates.

So only put something in a facet that would not be constant on either node but only constant on the edge of a node. This would normally be modeled in GraphQL as Person<->Friendship<->Person to keep track of edge data such as the weight of the friendship, relationship status between the friends, etc. But with facets it can be a simple single edge simplifying mutations and deep filters. The single edge relationships can stay just that. And attributes can be added later on without needing to rebuild the schema and refactor all friend relationahips.

2 Likes

Yeah, facets don’t increase the expressive power of the graphql. The motivation behind them is to add an intuitive description to edges and to support the Dgraph feature in GraphQL.

Also, it’s not always easy to model edge properties in graphql types.
For example, consider [scalar] case in the below schema

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: [String]
}

In order to model it without facets, we need to create separate type Pet explicitly.
we can also model it with separate array’s for petClose and petSince in same type but that can have mapping issues if some person has pet without petClose or petSince property.

Our implementation of facet creates separate types internally for the facet and gives a more intuitive view to User.

So, even if facets doesn’t increase the expressive power of language, they simplify the modeling of some cases and add support for Dgraph feature.

For data migration cases,

  • We just insert null values in since facet for the friends which are already there.
  • We have pet type and facets petSince and petClose on it. We can just add petToys in pet Type.

Sorry, don’t agree that it ‘helps’ you say things.

Sure, in something extra flexible like Dgraph, that’s true, but facets don’t exist in GraphQL, so all the examples above do actually require creating that node, so my client code and the GraphQL schema both really have that connecting node. e.g. this mutation

addPerson(input: [{name: "Carol", pet:{ value:"Jerry",petClose:true, petSince: 2006-01-02T15:04:05}}]) {

and this query

{
  queryPerson {
    name
    pet {
      value 
      petClose
     ...

So, yeah, I agree in general that it’s a neat graph modelling trick if you know what you are doing, but in GraphQL, you can’t really get around those extra nodes because the language doesn’t support the concept directly.

So I don’t agree with this because if I look at that [Scalar] example in the RFC, I find this:

mutation {
    addPerson(input: [{
         name: "Carol", pet: [
         { value: "Jerry", petClose: true, petSince: "2006-01-02T15:04:05" },
         { value: "Tom", petSince: "2007-01-02T15:04:05" },
         { value: "Donald", petClose: false }
       ]
     }]) 
    {
       numUids
    }
} 

which is exactly that you have to create objects of those extra types explicitly.

I think the tradeoff isn’t really worth it. It seems to be saying: hey, you can type an extra line’s worth of @facet(...) in your schema to save you having to type a line in your schema ?!? (Which is a one-off cost) But all the actual work you do many times in interacting with the API is exactly the same.

As per my note above the actual API and the queries and mutations are exactly the same. The difference is between

type Person {
    name: String!
    petClose: Boolean @facet(field: "pet", name: "close")
    petSince: DateTime @facet(field: "pet", name: "since")
    pet: String
}

and

type Person {
    name: String!
    pet: Pet
}

type Pet {
    close: Boolean 
    since: DateTime 
    name: String
}

So the difference is between cramming petClose: Boolean @facet(field: "pet", name: "close") into one line, and some magic happening, or just saying it directly.

Which looks to me like a tradeoff between saying something with some magic in it vs just saying what you mean and are gunna get anyway.

Totally agree that if we want to support Dgraph users to be able to export their existing facets into GraphQL, then this kind of thing is worth investigating, but an argument that it adds something to GraphQL or helps you do some sort of modelling in GraphQL, doesn’t hold water to me.

I am really enjoying the tension in this exchange - nice work everyone!

I think that one of the things which is so alluring about graph based data modeling is the capacity to describe data in ways that are closer to how our infinitely associative minds work.

This means that the arrow that connects nodes also has descriptive properties.
How we model this is up to us, as long as we don’t get stuck back into relational data table thinking, I can go along. Not using facets because (the current version of standard) GraphQL doesn’t support them is not an end game. We could also use an altered standard and push to expand the existing one…

That said, Here is a proposal that is more verbose and explicit, but can at least capture the essence of facet thinking in current GraphQL syntax:

type Person {
    name: String!
    petRelations: [PetRelation]
}

type PetRelation {
    pet: Pet!
    owner: Person!
    percentOwnership: Number         # relevant for shared ownership
    isClose: Boolean  
    since: DateTime 
}

type Pet {
    name: String!
    ownerRelations: [PetRelation]
    birthDate: DateTime
    weight: Int
}

The Dgraph schema with facets is still way more sexy, but @michaelcompton has some really solid points, eg:

Still, I think the Dgraph core team needs to be careful with how the DQL ↔ GQL relationship evolves.
I am now experimenting with using DQL nquads language and GraphQL in parallel as a learning exercise, but I think production systems will need to fully decide:
Do I:
a. Use GraphQL with poor man’s auth and no facets
OR
b. Use DQL with facets and enterprise ACL

Trying to support a hybrid system in production feels rather foolish. Therefore this WIP proposal may actually be more productive if it was about a migration strategy and support for that as a one-time event, rather than long term support for facets in GraphQL … as sad as it makes me to say so : (

1 Like

As an alternative to having indirection in the generated schema. We came up with another solution which adds facets in the type itself as discussed below,

UID

type Person {
    name: String!
    pet: Animal
}
type Animal {
    id: ID!
    name: String!
    color: String
    petClose: Boolean! @facet(field: "pet" ,name: "close")
    petSince: DateTime @facet(field: "pet" ,name: "since")
}

If a type Animal is referenced by many other types then we need some method to know which facet is for which type. As of now, we can allow every facet to be used by any type. We need to keep track of the referencing type and can do it while doing mutation.

Adding values for petClose, petSince and petFav while doing an addAnimal or updateAnimal mutation would be a No-op since these facets are supposed to exist on an edge between another type (say Person) and Animal.

Generated schema

input AddPersonInput {
    name: String!
    pet: AnimalRef
}
input AnimalRef {
    id: ID!
    name: String!
    color: String
    petClose: Boolean
    petSince: DateTime
    petfav: String
}
type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: Animal
}

Mutation

mutation {
  addPerson(input: [{
      name: "Carol", pet:{name:"Jerry",color:"Gray",petClose:true,petSince: "2006-01-02T15:04:05"}}]) {
      person {
      ...
    }
  }
}

rewrites to

{
  "uid": "_:Carol",
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": {
    "uid": "_:Jerry",
    "Animal.name": "Jerry",
    "Animal.color": "Gray",
    "Animal.pet|close": true,
    "Animal.pet|since": "2006-01-02T15:04:05",
    "dgraph.type": "Animal"
  }
}

GraphQL query

{
  queryPerson {
    name
    pet {
      name
      color
      petClose
      petSince
    }
  }
}

would be rewritten to

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(close,since) {
       Animal.name
       Animal.color
     }
   }
}

Dgraph Response

{
  "data": {
    "queryPerson": [
      {
        "Person.name": "Carol",
        "Person.pet": {
          "Animal.name": "Jerry",
          "Animal.color": "Gray",
          "Animal.pet|close": true,
          "Animal.pet|since": "2006-01-02T15:04:05"
        }
      }
    ]
  }
}

GraphQL Response

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": {
            "name": "Jerry",
            "color": "Gray"
            "petClose": true,
            "petSince": "2006-01-02T15:04:05"
        }
      }
    ]
  }
}

[UID]

type Person {
    name: String!
    pet: [Animal]
}
type Animal {
    id: ID!
    name: String!
    color: String
    petClose: Boolean! @facet((field: "pet" ,name: "close")
    petSince: DateTime @facet(field: "pet" ,name: "since")
}

Generated schema

input AddPersonInput {
    name: String!
    pet: [AnimalRef]
}
input AnimalRef {
    id: ID!
    name: String!
    color: String
    petClose: Boolean
    petSince: DateTime
}
type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: [Animal]
}

The following GraphQL mutation

mutation {
  addPerson(input: [{
    name: "Carol", pet: [
    {name: "Jerry", petClose:true, petSince: "2006-01-02T15:04:05"},
    {name: "Tom", color: "Grey", petClose:false, petSince: "2007-01-02T15:04:05"},
    {name: "Donald", petClose:false, petSince: "2008-01-02T15:04:05"}, }]}]) {
    numUids
  }
}

would be rewritten to this DQL mutation

{
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": [
    {
      "Animal.name": "Jerry",
      "Animal.pet|close": true,
      "Animal.pet|since": "2006-01-02T15:04:05",
      "dgraph.type": "Animal"
    },
    {
      "Animal.name": "Tom",
      "Animal.color": "Grey",
      "Animal.pet|close": false,
      "Person.pet|since": "2007-01-02T15:04:05",
      "dgraph.type": "Animal"
    },
    {
      "Animal.name": "Donald",
      "Animal.friend|close": "false",
      "Animal.pet|since": "2008-01-02T15:04:05",
      "dgraph.type": "Animal"
    }
  ]
}

GraphQL query

{
  queryPerson {
    name
    pet {
      name
      color
      petClose
      petSince
    }
  }
}

would be rewritten to

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(close,since) {
       Animal.name
       Animal.color
     }
   }
}

Dgraph Response:

{


  "data": {
    "queryPerson": [
      {
        "Person.name": "Carol",
        "Person.pet": [
          {
            "Animal.name": "Jerry",
            "Animal.pet|close": true,
            "Animal.pet|since": "2006-01-02T15:04:05"
          },
          {
            "Animal.name": "Tom",
            "Animal.color": "Grey",
            "Animal.pet|close": false,
            "Animal.pet|since": "2007-01-02T15:04:05"
          },
          {
            "Animal.name": "Donald",
            "Animal.friend|close": "false",
            "Animal.pet|since": "2008-01-02T15:04:05"
          }
        ]
      }
    ]
  }
}

GraphQL Response:

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": [
          {
            "name": "Jerry",
            "color": null
            "petClose": true,
            "petSince": "2006-01-02T15:04:05"
          },
          {
            "name": "Tom",
            "color": "Grey"
            "petClose": false,
            "petSince": "2007-01-02T15:04:05"
          },
          {
             "name": "Donald",
             "color": null
            "petClose": false,
            "petSince": "2008-01-02T15:04:05"
          }
        ]
      }
    ]
  }
}

Scalar

type Person {
    name: String!
    pet: String
    petClose: Boolean! @facet(field: "pet" ,name: "close")
    petSince: DateTime @facet(field: "pet" ,name: "since")   
}

Generated schema

input AddPersonInput {
    name: String!
    pet: String
    petClose: Boolean
    petSince: DateTime
}

type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: String
    petClose: Boolean
    petSince: DateTime
}

The following GraphQL mutation

mutation {
  addPerson(input: [{
    name: "Carol", pet:"Jerry",petClose:true, petSince: 2006-01-02T15:04:05]) {
    person {
      ...
    }
  }
 }
}

would be rewritten to this DQL mutation

{
  "uid": "_:Carol",
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": "Jerry",
  "Person.pet|since": "2006-01-02T15:04:05",
  "Person.pet|close": true
}

The GraphQL query

{
  queryPerson {
      name
      pet
      petClose
      petSince
    }
  }
}

would be rewritten to

{
    queryPerson(func: type(Person)) {
      Person.name
      Person.pet @facets(close,since)
   }
}

Dgraph Response

{
  "data": {
    "queryPerson": [
      {
        "Person.name": "Carol",
        "Person.pet": "Jerry",
        "Person.pet|close": true,
        "Person.pet|since": "2006-01-02T15:04:05"
      }
    ]
  }
}

GraphQL Response

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
        "pet": "Jerry",
        "petClose": true,
        "petSince": "2006-01-02T15:04:05"
        }
      }
    ]
  }
}

[Scalar]

type Person {
    name: String!
    pet: [String]
    petClose: [Boolean] @facet(field: "pet" ,name: "close")
    petSince: [DateTime] @facet(field: "pet" ,name: "since")
}

So the facets are also an array like the scalar value where the index in the array for a facet must match the index in the array for a value. This does mean that if the user only wanted to insert a facet for one of the values, then they’ll have to supply null for other values. We expect use cases with scalar lists and facets to not be very common hence this should be ok.

Generated schema

input AddPersonInput {
    name: String!
    pet: [String]
    petClose: [Boolean]
    petSince: [DateTime]
}

type AddPersonPayload {
    person: PersonPayload
    numUids: Int
}
type PersonPayload {
    name: String!
    pet: [String]
    petClose: [Boolean]
    petSince: [DateTime]
}

The following GraphQL mutation

mutation {
    addPerson(input: [{
         name: "Carol",
         pet: ["Jerry","Tom","Donald"],
         petClose:[true,null,false],
         petSince:["2006-01-02T15:04:05","2007-01-02T15:04:05",null]
       ]
     }]) 
    {
       numUids
    }
}

would be rewritten to this DQL mutation

{
  "Person.name": "Carol",
  "dgraph.type": "Person",
  "Person.pet": [
    "Jerry",
    "Tom",
    "Donald"
  ],
  "Person.pet|close": {
    "0": true,
    "2": false
  },
  "Person.pet|since": {
    "0": "2006-01-02T15:04:05",
    "1": "2007-01-02T15:04:05"
  }
}

The GraphQL query

{
  queryPerson {
      name
      pet
      petClose
      petSince
  }
}

would be rewritten to

{
   queryPerson(func: type(Person)) {
     Person.name
     Person.pet @facets(close,since)
   }
}

Dgraph Response:

{
  "data": {
    "queryPerson": {
      "Person.name": "Carol",
      "Person.pet": [
        "Jerry",
        "Tom",
        "Donald"
      ],
      "Person.pet|close": {
        "0": true,
        "2": false
      },
      "Person.pet|since": {
        "0": "2006-01-02T15:04:05",
        "1": "2007-01-02T15:04:05"
      }
    }
  }
}

GraphQL Response:

{
  "data": {
    "queryPerson": [
      {
        "name": "Carol",
         pet: ["Jerry","Tom","Donald"],
         petClose:[true,null,false],
         petSince:["2006-01-02T15:04:05","2007-01-02T15:04:05",null]
      }
    ]
  }
}

Pros:

  • The biggest advantage of using this design is that Graphql requests and response with facets are similar to Dgreaph request and response. So, users won’t have much difficulty porting to Graphql facets.

  • Rewriting graphql queries and mutations in this case are easy because we don’t need to change the generated schema.

Cons:

  • Updating facets for [Scalar] type is a bit verbose and would require sending null values in some cases.

yeah, agree with your points.
Our main motivation is to add support for dgraph facets that user can use in graphql.
Also we are trying best to do it with minimal rewriting and also to make the request and response with facets similar to dgraph, so that users don’t have much difficulty using facets in Graphql.

Ha, thanks, probably cause I’m typing too quickly and so sounding stern or something :slight_smile:

I think we are asking the wrong question here. We seem to be asking the question: “how can we squash facets into GraphQL”, but that’s just not a thing. And leads to things like trying to squash the facets for since and close onto the Animal type when it’s really a property that only makes sense of the relationship between a person and the pet they own.

I think instead we should be asking the a question like “if a Dgraph user has facets in their database, how can we allow them to query and mutate those facets through GraphQL”.

That’s a totally different thing to adding facets to GraphQL. For me, it’s like the difference in if I had an SQL database behind my GraphQL and saying “how do I represent tables in GraphQL”, Vs saying “how do I represent the data in my database as a GraphQL API”.

There’s then some fixed constraints. Such as, the way Dgraph represents facets, just isn’t GraphQL compatible. E.G. This https://dgraph.io/docs/mutations/json-mutation-format/#facets-in-list-type-with-json is like

"name": "Julian",
"nickname": ["Jay-Jay", "Jules", "JB"],
"nickname|kind": {
        "0": "first",
        "1": "official",
        "2": "CS-GO"
}

Which requires translating to another format to export in a GraphQL compatible way. Other fixed constraints are around that you must have a GraphQL schema with the types etc set.

So the, GraphQL valid, return data for above simply has to be something like

"name": "Julian",
"nicknames": [
  { 
     nickname: "Jay-Jay", 
     kind: "first"
  },
  { 
     nickname: "Jules", 
     kind: "official"
  },
  ...
]

So, for me, the way to model that kind of structure in GraphQL is like

type Person {
  ...
  nicknames: [NickName]
}

type NickName {
  nickname: String 
  kind: String 
}

The question is then, not how do we squash this into something that looks like facets (but has to be turned into GraphQL anyway), but rather how do we tell Dgraph to get the data that backs up the GraphQL API from a facet rather than from a node’s predicates.

With that in mind, I’d go for something like

type Person {
  ...
  nicknames: [NickName]
}

type NickName {
  name: String @dgraph(pred: nickname)
  kind: String @dgraph(facet: kind)
}

or something similar that lets the user model the GraphQL API they want and lets Dgraph recognise to get the data from a facet and not a node when we grab the data out of the database - it’s about what’s the implementation of these types, not how can I write these types to look like a facet.

That’s more along the lines of “here’s a GraphQL API, it’s got these types. Oh, and, Dgraph, I’m just letting you know that the implementation of those types is in these facets” … in much the same way as we use @dgraph(pred: ...) to say here’s an API, but the implementation of the API has different names to the external representation. … same as we use @custom to say, hey this bit isn’t in Dgraph, get it through some other process.

I think for me, the answer should look like natural modelling in GraphQL, but just has some annotation to let Dgraph know how to interpret the types (that might require some rules around where you can use things that map to facets).

2 Likes

I am really liking this suggestion out of the whole thread so far. This is really the summarization of the problem and the solution.

Furthermore, I believe that a facet should also be tied to a parent type and not just it’s type. A facet is information stored on the edge, so in GraphQL edges exist 1) between two types and 2) between a type and it’s predicates.

So a NickName type could be a child of many different Types. All of these parent types might not have the same facets on their edges as it just doesn’t make sense.

Take for example the friend facet data:

type Person {
  id: ID
  name: String!
  friends: [Person]
  parents: [Person]
  children: [Person]
  close: Boolean @dgraph(facet: "close", on: ["Person.friends"])
}
type Business {
  id: ID
  name: String!
  employees: [Person]
}

This would then notate that the Person.close would only be applicable to query if the parent edge was coming from Person.friends, but not on Person.parents, Person.children, nor Business.employees.

If this is implemented well, this could go great measures to simplify (and complicate) schemas with a use case such as keeping a type of relationship on the edge.

enum RelationshipType {
  friend
  parent
  child
  coworker
}
type Person {
  id: ID
  name: String!
  relationships: [Person]
  close: Boolean @dgraph(facet: "close", on: ["Person.relationships"])
  relationshipType: RelationshipType @dgraph(facet: "type", on: ["Person.relationships"])
}

Another thing that quickly becomes problematic without the on data is how to enforce a required facet. If the Person is queries at the root then the facet will always be null, so a required facet will always be missing here. And if a Person is subQueried under Business.employees they would not have the same facet requirements as if they were under Person.friend

yeah, we have discussed this point and find out that facets inside Animal type are easy to handle and solve many problems. We need to store the facets somewhere and it’s not a good idea to store them inside types which reference Animal.

The next thing is representing facets in graphql. I think we can change the @facets directive with @dgraph(facet:) and rest of the things will be same.

And we will convert the Dgraph response to a valid Graphql response as discussed in detail in previous comments.

We still have similar way to field on which field is specified. For example, we specify this in field argument to @facet.

 type Animal {
    id: ID!
    name: String!
    color: String
    petClose: Boolean! @facet(field: "pet" ,name: "close")
    petSince: DateTime @facet(field: "pet" ,name: "since")
}

We need to explicitly query the facet in order for them to included in the query response. So, null can only be returned from the response if the predicate doesn’t have queried facet.

Fair point. @JatinDevDG is going to provide an example of how this modelling might look if done through the @dgraph directive. The [scalar] list case is a bit tricky because in the example that you mentioned above, it looks like we are creating a new node for nickname. Hence there is no way of differentiating between [uid] and [scalar] for facets.

If we do provide an on argument, what is the expected behaviour? Do you expect the API to throw an error if a user queries or mutates the facet along an edge it is not supposed to? We don’t really need the on argument as we expect the user to know which edge a facet belongs to and expect them to only provide it on mutations/queries along that edge.

Given that the user would know that they don’t have facets at root, they won’t be querying them I suppose. That’s a bad query I would say?