GraphQL Viewer

The GraphQL viewer automatically exposes an Apache Causeway domain object model as a GraphQL API. The viewer iterates over the domain services and domain objects, with their actions, properties and collections represented as fields within the graph. Actions that take parameters are mapped to fields with arguments.

The GraphQL viewer is implemented using Spring GraphQL project, which in turn is built on top of GraphQL Java. This also means that it automatically provides the GraphiQL interactive query tool.

Concepts

API Variants

The GraphQL viewer supports 3 different API variants, controlled by the causeway.viewer.graphql.api-variant configuration property:

  • QUERY_ONLY

    Exposes only a Query API, of properties, collections and safe (query-onl) actions. Any actions that mutate the state of the system (in other words are idempotent or non-idempotent are excluded from the API, as is the ability to set properties.

  • QUERY_AND_MUTATIONS

    Exposes an API with Query for query/safe separate queries and field access, with mutating (idempotent and non-idempotent) actions and property setters instead surfaced as Mutations, as per the GraphQL spec.

  • QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT

    This variant exposes an API with both Query and Mutations, but relaxes the constraints for the Query API by also including idempotent and non-idempotent actions and property setters. The actions are also available through as Mutations (same as with QUERY_AND_MUTATIONS API variant).

    Examples of using this API variant are provided in the Queries that are also Mutations section, below.

    Be aware that the resultant API is not compliant with the rules of the GraphQL spec; in particular, it violates 2.3 Operations which states: "a query [is] a read‐only fetch";

In summary, the API variant therefore relates to whether and how mutating (non-safe) actions are represented.

Rich vs Simple Schemas

The viewer can represent the domain model as either a "rich" GraphQL schema, or a "simple" GraphQL schema; or indeed both can be supported ath the same time.

The rich schema represents all the behaviour and structure of the Causeway domain model:

  • hidden, disabled, validate

  • choices, autoComplete, defaults

  • obtain ("get") the value

In other words, it exposes not only the core data/behaviour but also of the supporting methods.

The simple schema represents just the core data/behaviour, but does not include the supporting method facets.

With the rich schema, the application logic and business domain logic remains the concern of the server, with the calling client app responsible only for presentation logic. In fact, the rich schemas is so rich that one could in theory implement a fully generic UI client.

With the simple schema, although the main business domain logic remains the concern of the server, the application logic moves to the client. For example, if there is a drop-down list box, the client needs to know how to obtain the list of choices. But the benefit of the simple schema is, well, that it is simpler; in other words more "intuitive" and less "verbose".

The causeway.viewer.graphql.schema-style configuration property specifies which schema (or both) is supported:

  • SIMPLE_ONLY - exposes only the "simple" schema

  • RICH_ONLY - exposes only the "rich" schema

  • SIMPLE_AND_RICH - exposes both the simple and rich schemas, the top-level query for each residing under either the simple or rich field respectively.

    For the top-level mutation, the "simple" schema is used.

  • RICH_AND_SIMPLE - also exposes both the simple and rich schemas, the top-level query for each residing under either the simple or rich field respectively.

    For the top-level mutation, the "rich" schema is used.

If only the simple or the rich schema is enabled (SIMPLE_ONLY or RICH_ONLY), then the set of types defined by each of these schemes is available at the root query. If both schemas are enabled (SIMPLE_AND_RICH or RICH_AND_SIMPLE), the these each reside under a top-level field, by default "simple" or "rich". The name of these fields can be configured to something else if required using causeway.viewer.graphql.top-level-field-name-for-simple and causeway.viewer.graphql.top-level-field-name-for-rich configuration properties.

Example

How the viewer works is probably most easily explained by an example. The diagram below shows a simple domain (in fact, this is the domain used by the GraphQL viewer’s own tests):

test domain.drawio

GraphQL distinguishes queries and mutations, so let’s look at each. We’ll assume here that the SIMPLE_AND_RICH schema style is in use.

GraphQL also defines the notion of subscriptions; the GraphQL viewer currently has no support for these.

Queries

Queries most often start at a domain service. In the example above, these would be Departments, DeptHeadRepository, or Staff.

To list all Departments, we can submit this query using either the "rich" schema or "simple" schema:

Rich schema Simple schema
{
  rich {                          (1)
    university_dept_Departments { (2)
      findAllDepartments {        (3)
        invoke {                  (4)
          results {               (5)
            name {
              get                 (6)
            }
            staffMembers {
              get {               (7)
                name {
                  get             (8)
                }
                _meta {
                  id              (9)
                  logicalTypeName (9)
                }
              }
            }
          }
        }
      }
    }
  }
}
{
  simple {                        (1)
    university_dept_Departments { (2)
      findAllDepartments {        (3)(4)(5)
        name                      (6)
        staffMembers {            (7)
          name                    (8)
          _meta {
            id                    (9)
            logicalTypeName       (9)
          }
        }
      }
    }
  }
}
1 specify schema style
2 domain service
3 action name
4 invokes the action
5 returning a list of Departments
6 gets (accesses) the name property of each returned Department
7 also gets (accesses) the staffMembers collection of each returned Department, returning a list of StaffMembers
8 gets the name prperty for each returned StaffMember
9 returns the internal id and logicalTypeName of each StaffMember. Together, these make up the Bookmark of the domain object.

Queries can also include parameters.

Rich schema Simple schema
{
  rich {
    university_dept_Departments {
      findDepartmentByName {
        invoke(name: "Classics") {
          results {
            name {
              get
            }
          }
        }
      }
    }
  }
}
{
  simple {
    university_dept_Departments {
      findDepartmentByName(name: "Classics") {
        name
      }
    }
  }
}

The above is an example of invoking an action on a (singleton) domain service, but this works equally well on domain entities/view models once retrieved. More on this below.

Supporting Metadata

If you use the "rich" schema, then as well as accessing properties and collections and invoking (safe) actions, the GraphQL viewer also allows access to the usual supporting metadata. For example:

Rich schema Simple schema
{
  rich {
    university_dept_Departments {
      findAllDepartments {
        disabled          (1)
        invoke {
          results {
            name {
              hidden        (2)
            }
          }
        }
      }
    }
  }
}

Not supported by simple schema.

1 whether this action is disabled
2 whether the property of the resultant object is hidden

Similarly, there are fields for action parameters:

  • validate - is the proposed action parameter valid?

  • disable - is the action or action parameter disabled?

  • choices - for an action parameter, are their choices?

  • autoComplete - for an action parameter, is there an auto-complete?

  • default - for an action parameter, is there a default value?

There are also similar fields for properties:

  • validate - is the proposed value of the property valid?

  • disable - is the property disabled?

  • choices - for a property, are their choices?

  • autoComplete - for a property , is there an auto-complete?

The Meta field/type

The _meta field provides access to additional information about the domain object. Its full list of fields are:

  • logicalTypeName and id; these are equivalent to the Bookmark of the domain object

  • version (if an entity and available)

  • title (as per the title() supporting method)

  • icon (as per the icon, normally the associated .png file) and grid (as per the layout, normally the associated .layout.xml file )

    These can only be downloaded if configured, see resources section below.

  • cssClass (as per the cssClass() supporting method)

There is also one additional field, saveAs; this is discussed in the Test Support section.

Queries that lookup a Domain Object

Most queries will start with a domain service, but it is also possible to define a query that starts with a "lookup" of existing domain object:

Rich schema Simple schema
{
  rich {
    university_dept_Department(   (1)
      object: {id: "1"}           (2)
    ) {
      name {
        get
      }
    }
  }
}
{
  simple {
    university_dept_Department(   (1)
      object: {id: "1"}) {        (2)
      name
    }
  }
}
1 logical type name of the domain object
2 identifier of the domain object instance

The next section explains how use mutations to change the state of the system.

Mutations

Actions that mutate the state of the system (with idempotent or non-idempotent @Action#semantics) are exposed as mutations. Editable properties are also exposed as mutations.

IF the action is on a domain service, then the target is implicit; but if the action is on a domain object — and also for properties — then the target domain object must be specified.

For example, to invoke a mutating action on a domain service:

Rich schema Simple schema
mutation {
  university_dept_Departments__createDepartment(  (1)
      name: "Geophysics",
      deptHead: null
  ) {
    name {
      get
    }
  }
}
mutation {
  university_dept_Departments__createDepartment(
      name: "Geophysics",
      deptHead: null
  ) {
    name
  }
}
1 derived from the logical type name of the domain service, and the action Id.

For example, to invoke a mutating action on a domain object

Rich schema Simple schema
mutation {
  university_dept_Department__changeName(     (1)
      _target: {id : "1"},                     (2)
      newName: "Classics and Ancient History"
  ) {
    name {
      get
    }
  }
}
mutation {
  university_dept_Department__changeName(
      _target: {id : "1"},
      newName: "Classics and Ancient History"
  ) {
    name
  }
}
1 derived from the logical type name of the domain object, and the action Id.
2 the object argument specifies the target object

Or, to set a property on a domain object:

Rich schema Simple schema
mutation {
  university_dept_StaffMember__name(  (1)
      _target: {id: "1"},             (2)
      name: "Jonathon Gartner"
  ) {
    name {                            (3)
      get
    }
  }
}
mutation {
  university_dept_StaffMember__name(
      _target: {id: "1"},
      name: "Jonathon Gartner"
  ) {
    name
  }
}
1 derived from the logical type name of the domain object, and the property Id.
2 the _target argument specifies the target object
3 property setters are void, so as a convenience the mutator instead returns the target object.

Queries that are also Mutations

According to the GraphQL specification, queries should be read-only; they must not change the state of the system.

Enabling the QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT API variant (also mentioned above) relaxes this rule, allowing actions to be invoked that do change the state of the system, and — indeed — allowing properties to be modified. This is done through these additional fields:

  • invokeIdempotent - to invoke an action whose action semantics are idempotent

    As specified by @Action#semantics.

  • invokeNonIdempotent - to invoke an action whose action semantics are non-idempotent

  • set - to modify a property.

For example, to invoke an action on a domain service:

Rich schema Simple schema
{
  rich {
    university_dept_Staff {
      createStaffMember {
        invokeNonIdempotent(
          name: "Dr. Georgina McGovern",
          department: { id: "1"}
      ) {
          results {
            name {
              get
            }
            department {
              get {
                name {
                  get
                }
              }
            }
          }
        }
      }
    }
  }
}
{
  simple {
    university_dept_Staff {
      createStaffMember (
          name: "Dr. Georgina McGovern",
          department: { id: "1"}
      ) {
        name
        department {
          name
        }
      }
    }
  }
}

Or, to find a domain object and then invoke a mutating action on it:

Rich schema Simple schema
{
  rich {
    university_dept_DeptHeads {
      findHeadByName {
        invoke(name: "Prof. Dicky Horwich") {
          results {
            changeName {
              invokeIdempotent(newName: "Prof. Richard Horwich") {
                results {
                  name {
                    get
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
{
  simple {
    university_dept_DeptHeads {
      findHeadByName(name: "Prof. Dicky Horwich") {
        changeName(newName: "Prof. Richard Horwich") {
          name
        }
      }
    }
  }
}

Or, similarly to find a domain object and then set a property afterwards:

Rich schema Simple schema
{
  rich {
    university_dept_Staff {
      findStaffMemberByName {
        invoke(name: "Gerry Jones") {
          results {
            name {
              set(name: "Gerald Johns") {
                name {
                  get
                }
              }
            }
          }
        }
      }
    }
  }
}

Not supported by simple schema

Resources (Blobs, Clobs, Layouts, Icons)

Rather than returning the values of Blobs and Clobs inline within a response, instead the GraphQL viewer renders these as a URL to a resource controller. The client can then make a second call to this endpoint using a simple HTTP(s) GET.

The same approach is used for both simple and rich schemas.

For example:

Rich schema Simple schema
{
  rich {
    university_dept_Staff {
      findStaffMemberByName {
        invoke(name: "Gerry Jones") {
          results {
            photo {
              get {
                bytes     (1)
              }
            }
          }
        }
      }
    }
  }
}
1 requests a URL to download bytes
{
  simple {
    university_dept_Staff {
      findStaffMemberByName(name: "Gerry Jones") {
        photo {
          bytes           (1)
        }
      }
    }
  }
}

This will result in a response (for the rich schema) such as:

{
  "data" : {
    "rich" : {
      "university_dept_Staff" : {
        "findStaffMemberByName" : {
          "invoke" : {
            "results" : {
              "photo" : {
                "get" : {
                  "bytes" : "///graphql/object/university.dept.StaffMember:123/photo/blobBytes"
                }
              }
            }
          }
        }
      }
    }
  }
}

The simple schema’s response is very similar.

The viewer does not currently provide any way to update Blobs or Clobs. One option is to implement a custom controller that the client can post to, analogous to the in-built resource controller.

The meta field mentioned earlier also allows the icon and grid (layout) files to be downloaded in a similar way:

Rich schema Simple schema
{
  rich {
    university_dept_Staff {
      findStaffMemberByName {
        invoke(name: "Gerry Jones") {
          results {
            _meta {
              icon        (1)
              grid        (2)
            }
          }
        }
      }
    }
  }
}
1 requests a URL to download bytes
{
  simple {
    university_dept_Staff {
      findStaffMemberByName(name: "Gerry Jones") {
        _meta {
          icon            (1)
          grid            (2)
        }
      }
    }
  }
}
1 URL to download the icon (typically a .png file)
2 URL to download the grid layout (typically the .layout.xml file)

Because the resource controller exposes information directly, these fields are suppressed by default. To enable, use the causeway.viewer.graphql.resources.response-type configuration property. If you do this, then you should also make sure that the resource controller is made secure in some appropriate fashion.