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
:
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:
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:
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:
type pets_Pet {
name: String!
notes: String
petOwner: pets_PetOwner! (1)
petSpecies: PetSpecies!
}
1 | cycles are permissible, though perhaps inadvisable. |
with PetSpecies an enum:
enum PetSpecies {
Dog
Cat
Hamster
Budgerigar
}
And Visit is:
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:
type pets_PetOwner {
# ...
_gql_generic: pets_PetOwner__DomainObject_generic! (1)
}
1 | type corresponds to classes annotated with @DomainObject`[2] |
where:
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!
}
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:
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:
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 |
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:
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:
type pets_PetOwners {
# ...
_gql_mutations: pets_PetOwners__DomainService_mutators!
}
where the mutators type lists all the mutating actions:
type pets_PetOwners__DomainService_mutators {
create(lastName: String!, ): pets_PetOwner!
}
Another example is the action to update the name of a 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:
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:
type pets_PetOwner {
# ...
_gql_generic: pets_PetOwner__DomainObject_generic! (1)
}
The generic type lists each of the actions:
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:
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:
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.
-
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" } } } } } }
-
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 } } } } } } }
-
… or we could use the shorter variant shown earlier:
{ _gql_lookup { pets_PetOwner(id: "12321") { _gql_mutations { updateName(lastName: "Smith", firstName: "Marianne") { lastName firstName } } } } }
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:
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 …