Skip to content

Instantly share code, notes, and snippets.

@kevinmichaelchen
Last active June 14, 2023 19:22
Show Gist options
  • Save kevinmichaelchen/6952db8ec370257dad58879bceb3a749 to your computer and use it in GitHub Desktop.
Save kevinmichaelchen/6952db8ec370257dad58879bceb3a749 to your computer and use it in GitHub Desktop.
Polymorphic GraphQL mutations with Hasura bulk inserts and Genqlient

Hasura provides an auto-generated GraphQL API over any (most) databases.

GitHub Issue

Khan/genqlient#272 (comment)

Example Domain

Consider a Postgres database with the following tables:

  • author
  • article

Transactional bulk inserts

Hasura provides the means to perform transactional bulk inserts using GraphQL.

mutation CreateStuff(
  $authors: [author_insert_input!]!
  $articles: [article_insert_input!]!
) {
  insert_author(
    objects: $authors
  ) {
    affected_rows
  }
  
  insert_article(
    objects: $articles
  ) {
    affected_rows
  }
}

with variables:

{
  "authors": [
    {"id": 5, "name": "Jacob"}
  ],
  "articles": [
    {"id": 1, "title": "Sample article 1", "content": "Some content", "author_id": 5},
    {"id": 2, "title": "Sample article 2", "content": "Some content", "author_id": 5}
  ]
}

What about polymorphism?

What if I want to use the same mutation for a variety of different situations:

  • when the author has some articles
  • when the author has zero articles

When running the mutation in the Hasura Console, in Postman, or in any GraphQL playground, we can provide the following JSON just fine:

{
  "authors": [
    {"id": 5, "name": "Jacob"}
  ],
  "articles": []
}

Does this work in genqlient?

genqlient provides directives we can use to indicate nullability. By annotating certain mutations or certain arguments, we can indicate that they should be omitted "if empty." For example, if we're building out our articles array in Go and end up with zero elements, we should send nothing (like the empty array [] shown above).

The problem is that Hasura's "insert input" arguments are typed as non-nullable arrays with non-nullable elements... And, unfortunately, genqlient does not allow the omitempty directive for such arguments:

omitempty may only be used on optional arguments
@benjaminjkraft
Copy link

Just to make sure I understand, is it that you're trying to send articles: [] and genqlient sends articles: null? Or vice versa? Or something else?

Assuming it's one of those, you can control this manually by setting the value to an empty slice (whereas by default I think it's nil) but I can see how that would be inconvenient. And assuming it's [] you want, we can probably fix that up in marshal because passing null is indeed incorrect.

@kevinmichaelchen
Copy link
Author

you're trying to send articles: []

Yup, that's the goal

and genqlient sends articles: null?

Yup. I think the goal is to have it send the empty slice in Go instead of null.

Assuming it's one of those, you can control this manually by setting the value to an empty slice (whereas by default I think it's nil) but I can see how that would be inconvenient.

I'll do a sanity-check and report back here! Thought I tried that 😆

@kevinmichaelchen
Copy link
Author

kevinmichaelchen commented May 24, 2023

Apologies for the change of domain from authors/articles to pets...

In genqlient.graphql, I have a mutation that tries to do a bulk insert against Hasura:

mutation CreatePets(
  $objects: [PetInsertInput!]!
) {
  pets: insertPet(objects: $objects) {
    returning {
      id
    }
  }
}

In my Go code, I'm passing in an empty slice:

inputs := []*graphql.PetInsertInput{}
_, err := graphql.CreatePets(ctx, s.client, inputs)

However, that's returning an error.

expecting a value for non-nullable variable: "objects"

@kevinmichaelchen
Copy link
Author

kevinmichaelchen commented May 24, 2023

Digging into it, I'm seeing the issue is that the omitempty tag might be causing null to get sent instead of an empty array.

// __CreatePetsInput is used internally by genqlient
type __CreatePetsInput struct {
	Objects []*PetInsertInput `json:"objects,omitempty"`
}

Here are the configs I'm using

optional: pointer
use_struct_references: true

If I disable use_struct_references, I get

// __CreatePetsInput is used internally by genqlient
type __CreatePetsInput struct {
	Objects []PetInsertInput `json:"objects"`
}

which seems like it would work, but at the expense of breaking other things

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment