[What's wrong with GraphQL] ... and how to deal with it

    In the past , we looked at uncomfortable moments in the GraphQL type system.
    And now we will try to defeat some of them. All interested, please under the cat.


    The numbering of sections corresponds to the problems that I managed to cope with.


    1.2 NON_NULL INPUT


    At this point, we looked at the ambiguity that the implementation feature of nullable in GraphQL generates.


    And the problem is that it does not allow to implement the concept of partial update (partial update) with a swoop - an analogue of the HTTP method PATCHin the REST architecture. In the comments on the past material I was strongly criticized for the "REST" thinking. I will only say that CRUD architecture obliges me to this. And I was not ready to give up the advantages of REST, simply because "do not do this." And the solution to this problem was found.


    And so, back to the problem. As we all know, the CRUD script, when updating a record, looks like this:


    1. We got a record from the back.
    2. Edited record fields.
    3. Sent entry to the back.

    The concept of partial update, in this case, should allow us to send back only those fields that have been changed.
    So, if we define an input model in this way


    input ExampleInput {
       foo: String!
       bar: String
    }  

    then when mapping a type variable ExampleInputwith this value


    { 
      "foo": "bla-bla-bla"
    }

    on a DTO with this structure:


    ExampleDTO {
       foo: String# обязательное поле
       bar: ?String  # необязательное поле
    }

    we get a DTO object with this value:


    {
       foo: "bla-bla-bla",
       bar: null
    }

    and when mapping a variable with this value


    { 
      "foo": "bla-bla-bla", 
      "bar": null
    }

    we get a DTO object with the same value as last time:


    {
       foo: "bla-bla-bla",
       bar: null
    }

    That is, entropy occurs - we lose the information about whether the field was transferred from the client or not.
    In this case, it is not clear what needs to be done with the target object field: do not touch it because the client did not pass the field, or set a value to it null, because the client passed it null.


    Strictly speaking, GraphQL is an RPC protocol. And I began to think about how I was doing such things on the back-up and what procedures I should call to do exactly as I wanted. And on the back end, I do a partial update of the fields like this:


    $repository->find(42)->setFoo('bla-bla-lba');

    That is, I literally do not touch the setter of an entity property if I don’t need to change the value of this property. If you pass it on to the GraphQL scheme, you get this result:


    type Mutation {
       entityRepository: EntityManager!
    }
    type EntityManager {
      update(id: ID!): PersitedEntity
    }
    type PersitedEntity {
      setFoo(foo: String!): String!
      setBar(foo: String): String
    }

    now, if we want, we can call the method setBar, and set its value to null, or not touch this method, and then the value will not be changed. Thus, there is a good implementation partial update. No worse than PATCHthe notorious REST.


    In the comments to the past material, summerwind asked: what is needed partial update? I answer: there are VERY big fields.

    3. Polymorphism


    It often happens that you need to submit to the input entities that are “the same” but not quite. I will use the example of creating an account from the past material.


    # аккаунт организации
    AccountInput {
        login: "Acme",
        password: "***",
        subject: OrganiationInput {
            title: "Acme Inc"
        }
    }

    # аккаунт  частного лица
    AccountInput {
        login: "Acme",
        password: "***",
        subject: PersonInput {
            firstName: "Vasya",
            lastName: "Pupkin",
        }
    }

    Obviously, we cannot submit data with such a structure to one argument — GraphQL simply will not allow us to do this. So you need to somehow solve this problem.


    Method 0 - in the forehead


    The first thing that comes to mind is the separation of the variable part of the input:


    input AccountInput {
       login: String!
       password: Password!
       subjectOrganization: OrganiationInput
       subjectPerson: PersonInput
    }

    Hmm ... when I see such a code, I often remember Josephine Pavlovna. It does not suit me.


    Method 1 - not in the forehead, but in the forehead
    Here the fact came to my aid that I use I use UUID to identify entities (in general, I recommend it to everyone - it will help out more than once). And this means that I can create valid entities directly on the client, link them together by an identifier, and send them to the back-end, separately.


    Then we can do something in the spirit:


    input AccountInput {
       login: String!
       password: Password!
       subject: SubjectSelectInput!
    }
    input SubjectSelectInput {
       id: ID!
    }
    type Mutation {
       createAccount(
         organization: OrganizationInput,  
         person: PersonInput,  
         account: AccountInput!
       ): Account!
    }

    or, which turned out to be more convenient (why it is more convenient, I will tell when we get to the generation of user interfaces), divide it into different methods:


    type Mutation {
       createAccount(account: AccountInput!): Account!
       createOrganization(organization: OrganizationInput!): Organization!
       createPerson(person: PersonInput!) : Person!
    }

    Then, we will need to send a request to createAccount and createOrganization / createPerson in
    one batch. It is worth noting that then the batch processing must be wrapped in a transaction.


    Method 2 - magical scalar
    point is that in scalar GraphQL, it's not just Int, Sting, Floatetc. This is generally anything (well, as long as JSON can do it, of course).
    Then we can simply declare a scalar:


    scalar SubjectInput

    Then, write your handler to it, and do not soar. Then we can easily slip variable fields on input.


    Which way to choose? I use both, and have developed for myself the following rule:
    If the parent entity is Aggregate Root for the child, then I choose the second method, otherwise - the first one.


    4. Generics.


    Everything is trite here and I haven't invented anything better than code generation. And without Rails (the railt / sdl package) I could not do it (or rather, I would have done the same thing with crutches). The point is that Rail allows defining document-level directives (there is no such position in the spec for directives).


    directive @example on DOCUMENT

    That is, directives are not tied to anything other than the document in which they are called.


    I introduced the following directives:


    directive @defineMacro(name: String!, template: String!) on DOCUMENT
    directive @macro(name: String!, arguments: [String]) on DOCUMENT

    I think that nobody needs to explain the essence of macros ...


    That's all for now. I do not think that this material will cause as much noise as the last. All the same, the title there was pretty "yellow")


    In the comments to the past material, habrovchane stoked for sharing access ... it means the following material will be about authorization.


    Also popular now: