📜 ⬆️ ⬇️

[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 implementing the concept of partial update (partial update) with a swoop - an analogue of the HTTP PATCH method in 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 variable of type ExampleInput with 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 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 it to null , because the client passed null .


Strictly speaking, GraphQL is an RPC protocol. And I began to think about how I was doing such things on the back 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 setBar method, and set its value to null, or not touch this method, and then the value will not be changed. Thus, there is a not bad partial update implementation. Not worse than the PATCH of the notorious REST.


In the comments on the past material, summerwind asked: why do we need 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 on the forehead
Then I came to the aid of the fact that to identify the entities I use, I use the UUID (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 even more convenient (why it is more convenient, I will tell you 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
one batch. It is worth noting that then the batch processing must be wrapped in a transaction.


Method 2 - magic scalar
The point is that the scalar in GraphQL is not only Int , Sting , Float , etc. 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.


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.


')

Source: https://habr.com/ru/post/435690/


All Articles