Search code examples
scalagraphqlsangria

How can I give an ID to entities created in GraphQL using Sangria?


I have a case class of Inventory:

case class Inventory(
    organizationId: UUID,
    inventoryId: UUID,
    name: String,
    schema: String
)

An input type:

private val NewInventoryInputType =
    deriveInputObjectType[Inventory](
        InputObjectTypeName("NewInventory"),
        ExcludeInputFields("organizationId", "inventoryId")
    )

An argument:

val NewInventory = Argument("inventory", NewInventoryInputType)

And finally a field:

val MutationType = ObjectType("Mutation", fields[GraphQLContext, Unit](
    Field("createInventory", OptionType(UuidType),
        Some("Creates a new inventory."),
        NewInventory :: Nil,
        resolve = c => {
            val inventoryId = UUID.randomUUID
            val inventory = c arg NewInventory
            println(s"Inventory($inventory)")
            inventoryId
        }
    )
))

What I'd like to do is be able to create an Inventory with a query like this:

{
    "query": "mutation ($inventory: NewInventory!) { createInventory(inventory: $inventory) }",
    "variables": {
        "inventory": {
            "name":"i1",
            "schema":"s"
        }
    }
}

The missing piece is where to create the UUIDs for organizationId and inventoryId before Sangria attempts to instantiate an Inventory domain object using the variables it has.

Currently, I get this error:

Argument 'inventory' has invalid value: At path '/inventoryId': error.path.missing (line 1, column 67):
mutation ($inventory: NewInventory!) { createInventory(inventory: $inventory) }
                                                                  ^

(Of course, I could just create a NewInventory case class without the ID fields and instantiate an Inventory manually, but I'd like to avoid creating and maintaining two classes for each entity type.)


Solution

  • When you are defining an input object for a case class, you need to define 2 things:

    1. InputObjectType - it declares an input type in GraphQL schema and exposes it's meta-information via introspection API. In your example, you are defining it with a help of the deriveInputObjectType[Inventory] macro.
    2. An implicit instance of FromInput[Inventory] type class - it provides an information on how to deserialize this input object from some input, like JSON. As you mentioned elsewhere, you are already defining it with some JSON library (Json.format[Inventory])

    Since you are excluding 2 fields from the definition of the input object, you also need to ensure that deserializer knows about it and it should be able to handle a scenario where these 2 fields are missing. In your case class, these 2 fields are non-optional which means that JSON library (I'm assuming you are using play-json) will always expect these 2 fields to be present. This is why you see this error: Argument 'inventory' has invalid value: At path '/inventoryId'. The error actually comes from play-json because in GraphQL Schema you allowed this field to be absent but JSON deserializer does not know about it

    FromInput[Inventory] type class provides quite flexible mechanism though. I would suggest you to check it's documentation and maybe define your own implementation/wrapper that will handle these missing fields. An alternative approach would be to go more in direction of CQRS and define 2 separate modes: one for queries and another one for mutation inputs (which also can be represented in scala code as 2 separate case classes). Of course, you also can define both optional fields as Option[UUID].