Petclinic : GraphQL types

This page illustrates how the domain types that make up petclinic are surfaced in GraphQL.

Schema

There is always a top-level Query type:

schema {
    query: Query
}

The Query type has a field as an accessor for each domain service; in our case there is just one, PetOwners:

Query
type Query {
    pets_PetOwners : pets_PetOwners!  (1)
}
1 derived from the logical type name.

Domain Services

Each domain service therefore defines a GQL type. This surfaces the read-only actions (ie with SAFE semantics) as fields, usually with arguments.

For PetOwners, we have:

pets.PetOwners
type pets_PetOwners {
    findByLastNameLike(lastName: String!): [pets_PetOwner]!   (1)
    findByLastName(lastName: String!): [pets_PetOwner]!
    findByLastName: [pets_PetOwner]!
}
1 mandatory flag inferred from @LastName meta-annotation

The non-idempotent action create is discussed later.

Domain Objects

By domain object we mean either an entity or a view model.

For PetOwner, we have:

pets.PetOwner
type pets_PetOwner {    (1)
    lastName: String!
    firstName: String
    phoneNumber: String
    emailAddress: String
    pets: [pets_Pet]!
}
1 derived from the logical type name.

Similarly, Pet is:

pets.Pet
type pets_Pet {
    name: String!
    notes: String
    petOwner: pets_PetOwner!    (1)
    petSpecies: PetSpecies!
}
1 cycles are permissible, though perhaps inadvisable.

with PetSpecies an enum:

PetSpecies[1]
enum PetSpecies {
    Dog
    Cat
    Hamster
    Budgerigar
}

And Visit is:

visits.Visit
type visits_Visit {
    pet: pets_Pet!
    visitAt: String!
    reason: String
}

Domain object identity and version

All domain objects (entities and view models) have an id, while entities may also have a version.

Using the id it also possible to lookup domain objects from Query, see below.

All domain objects also have a "logical type name" which when combined with the id becomes a unique identifier to the object.

Because these attributes are (normally) not part of the domain, we provide access to them through a separate _gql_meta field. The "_gql" prefix is therefore reserved for GraphQLObjects.

Thus, the pets_PetOwner type becomes:

pets.PetOwner
type pets_PetOwner {
    # ...
    _gql_generic: pets_PetOwner__DomainObject_generic! (1)
}
1 type corresponds to classes annotated with @DomainObject`[2]

where:

pets.PetOwner’s generic type
type pets_PetOwner__DomainObject_generic {
    id: String!
    logicalTypeName: String!
    version: String!            (1)
}
1 only for entities defined with versioning; never for view models

Example

For example:

{
    pets_PetOwner {
        findByNameLike(lastName: "Smith") {
            lastName
            firstName
            phoneNumber
            _gql_generic {
                id
            }
        }
    }
}

could return:

{
    "data" : {
        "pets_PetOwner": {
            "findByNameLike": [
                {
                    "lastName": "Smith",
                    "firstName": "Mary",
                    "phoneNumber": "777 4321",
                    "_gql_generic" : {
                        "id" : "12321"
                        }
                    }
                },
                {
                    "lastName": "Smith",
                    "firstName": "John",
                    "phoneNumber": "555 1234",
                    "_gql_generic" : {
                        "id" : "12345"
                        }
                    }
                }
            ]
        }
    }
}

Input types for domain objects

A domain entity can be used as an action parameter. When we would like to give an existing pet a new owner for example:

type pets_PetOwner__DomainObject_mutators {
  # ...
  addPet(pet: [Type for Pet here]):pets_PetOwner

  }

an instance of the Pet entity can be passed in using input type

input _gql_input__pets_Pet {
    id : ID!
}

Example

For example:

could return:

Looking up arbitrary objects

If the id of a domain object is known, and its type, then it can be looked up directly from Query, without having to traverse a long graph.

To support this, Query defines its own reserved field, _gql_lookup, from which any object can be looked up.

For example:

Query
type Query {
    pets_PetOwners : pets_PetOwners!  (1)
    # ...
    _gql_lookup: _gql_Query_lookup    (2)
}
1 as before, repeated for all domain services
2 access to the lookup fields

where:

Query
type _gql_Query_lookup {
    pets_PetOwner(id: String) : pets_PetOwner   (1)
    pets_Pet(id: String) : pets_Pet
    visits_Visit(id: String) : visits_Visit
}
1 for each domain object

Example

We can therefore submit a query:

{
    _gql_lookup {
        pets_PetOwner(id: "12321") {
            lastName
            firstName
        }
    }
}

to obtain a response:

{
    "data" : {
        "_gql_lookup": {
            "pets_PetOwner": {
                "lastName": "Smith",
                "firstName": "Mary"
            }
        }
    }
}

Titles

The title() method returns a human readable label for a domain object. This need not be strictly unique, but needs to be "unique enough" that the user can readily recognise the object. As the method is (should be) read-only, it is surfaced under the _gql_generic field as part of the Xxx__DomainObject generic type.

For example:

pets.PetOwner’s DomainObject generic type
type pets_PetOwner__DomainObject_generic{
    id: ...
    # ...
    title: String!
}

Non-idempotent actions

Non-idempotent actions have side-effects and so are not part of the query graph. With GraphQLObjects we take a pragmatic approach and break this rule, and allow actions to be invoked as if they are fields. To reducethe chance of a mistake, these actions are placed under a special _gql_mutations field.

For example, the PetOwners domain service exposes the create(…​) action which has side-effects. This is represented as:

pets.PetOwners
type pets_PetOwners {
    # ...
    _gql_mutations: pets_PetOwners__DomainService_mutators!
}

where the mutators type lists all the mutating actions:

pets.PetOwners mutators type
type pets_PetOwners__DomainService_mutators {
    create(lastName: String!, ): pets_PetOwner!
}

Another example is the action to update the name of a PetOwner:

pets.PetOwner
type pets_PetOwner {
    # ...
    _gql_generic: pets_PetOwner__DomainObject_generic!         (1)
    _gql_mutators: pets_PetOwner__DomainObject_mutators! (1)
}
1 Note that the _generic and the _mutators are different types

where:

pets.PetOwner’s mutators type
type pets_PetOwner__DomainObject_mutators {
    updateName(lastName: String!, firstName: String!): PetOwner!
}

Example

To create a PetOwner we can submit this "query":

{
    pets_PetOwners {
        _gql_mutations {
            create("Brown", "Josh") {
                lastName
                firstName
                _gql_generic {
                    id
                }
            }
        }
    }
}

which would return the created object:

{
    "data": {
        "pets_PetOwners": {
            "_gql_mutations": {
                "create" : {
                    "lastName": "Brown",
                    "firstName": "Josh",
                    "_gql_generic" : {
                        "id" : "123"
                    }
                }
            }
        }
    }
}

Supporting Methods for Mutations

Defaults

The PetOwner’s `updateName(…​) action takes two arguments, both of which have a default value.

Access to this more complete metamodel is provided through the (previously mentioned) generic type:

pets.PetOwner
type pets_PetOwner {
    # ...
    _gql_generic: pets_PetOwner__DomainObject_generic!         (1)
}

The generic type lists each of the actions:

pets.PetOwner’s generic type
type pets_PetOwner__DomainObject_generic {
    properties {        (1)
        # ...
    }
    collections {
        # ...
    }
    actions {
        updateName: pets_PetOwner__updateName__ObjectAction_generic!
    }
}
1 fields also have formal representation, see below.

where we now have (yet another) type describing the action itself:

pets.PetOwner’s generic type
type pets_PetOwner__updateName__ObjectAction_generic {
    semantics: _gql_Semantics
    default0: String
    default1: String
    act(lastName: String!, firstName: String!): pets_PetOwner!  (1)
}
1 Provides an alternate way to invoke the action

The _gql_Semantics enum indicates whether accessing this gql field will have side-effects:

_gql_Semantics
enum _gql_Semantics {
    SAFE
    IDEMPOTENT
    NON_IDEMPOTENT
}

Example

Consider an action that takes arguments. We first would want to render a prompt page that uses the defaults to populate the argument values. When the user hits OK, we would then want to invoke the action itself.

  1. To render a prompt page for this action (using the lookup facility), we would submit the query:

    {
        _gql_lookup {
            pets_PetOwner(id: "12321") {
                _gql_generic {
                    actions {
                        updateName {
                            default0
                            default1
                        }
                    }
                }
            }
        }
    }

    and obtain a response

    {
        "data" : {
            "_gql_lookup": {
                "pets_PetOwner": {
                    "_gql_generic": {
                        "actions": {
                            "default0": "Smith",
                            "default1": "Mary"
                        }
                    }
                }
            }
        }
    }
  2. To then invoke the action the client we can either use the long formal style:

    {
        _gql_lookup {
            pets_PetOwner(id: "12321") {
                _gql_generic {
                    actions {
                        updateName {
                            act(lastName: "Smith", firstName: "Marianne") {
                                lastName
                                firstName
                            }
                        }
                    }
                }
            }
        }
    }
  3. …​ or we could use the shorter variant shown earlier:

    {
        _gql_lookup {
            pets_PetOwner(id: "12321") {
                _gql_mutations {
                    updateName(lastName: "Smith", firstName: "Marianne") {
                        lastName
                        firstName
                    }
                }
            }
        }
    }

Choices

TODO: Similar to defaults

Autocomplete

TODO: Similar to choices, though accepts an argument

Validate

TODO

Disable

TODO

Hide

TODO

NB: there are no examples of hideXxx in the domain model…​

Supporting Methods for Fields

Focus on read-only fields …​

Hide

TODO

Editable Fields

Editable fields are treated as overloaded fields:

  • with no-args, is the accessor (getter) and so modelled as a regular GraphQL field

  • with one-arg, is the setter and is modelled as a non-safe one-arg mutator

Default

TODO

Choices

TODO

Act

TODO

Validate

TODO

Disable

TODO

Mutation type

TODO: for those that don’t want to use the _gql_mutation hackery, also provide a mechanism to use the Mutation top-level.

In essence, the idea is that every mutating action is exposed on the Mutation type, but with a mangled name to ensure uniqueness and also with an id as the first param to perform a lookup.

For example, PetOwner#updateName(…​) becomes:

pets.PetOwner’s generic type
type Mutation {
    pets_PetOwner__updateName(id: String, lastName: String!, firstName: String!): pets_PetOwner!
    # ...
}

Idea’s

{
gqltestdomain_GQLTestDomainMenu {

     findE2(name:"foo") {
      name,
      _gql_generic {
        title,
        iconname,
        properties {
            ....
        },
        collections {
            ....
        },
        actions {
          changeE1 {
            params{
              #named param instead of default0
              firstname {
                optionality {
                  bool
                },

                default {
                  title,
                  iconname,
                  id
                  #or a value (Maybe call titled bookmark)
                },
                choices {
                    #list of gql_generic 's when entity
                    title,
                  iconname,
                    id
                   # or list of values
                },
                autoComplete(string : "xxx") {
                   #list of gql_generic 's
                    title,
                  iconname,
                    id
                  # or list of values
                },
                validate(String : value) {
									reason
                }
              },

              # .....

            },
            validate {
              string
            },
            hide {
              boolean
            },
            disable {
              string
            }




          }
        },
        logicalTypeName,

      }
    }


}

Give fields field an context param: WHERE.ANYWHERE etc …​



1. This should really be the fully qualified. Or, do we allow @LogicalTypeName on enums?
2. corresponds to `ObjectSpecification in the metamodel