directive @materializer
The @materializer directive specifies:
how a field is resolved by a field set selection (location
FIELD_DEFINITION);how a object type is reshaped to another type (location
OBJECT).
Field materialization
extend type P {
# f is resolved by an execution of Query.q
f:T @materializer(query:"q")
}
extend type Query {
q:T
}Arguments for the field set selection in query can be mapped using arguments.
If a mapping is not explicitly supplied for an argument, the following rules apply:
if the enclosing type of the annotated field is a root operation type (e.g.,
Query), the argument is mapped from an argument of the annotated field with the same name, if such an argument exists.if the enclosing type of the annotated field is not a root operation type, the argument is mapped from a field in the enclosing type of the annotated field with the same name if it exists.
For example:
type Customer {
id:ID!
email: String
emailInfo: EmailInfo
@materializer(query:"validateEmail")
}
type Query {
validateEmail(email:String!): EmailInfo
@rest(...)
}In this case, the field Customer.emailInfo is materialized
by the execution of field Query.validateEmail, with the argument email being
set from Customer.email of the same object.
If a field f in a concrete (object) type C is not
annotated with @materializer then it inherits
any @materializer specified on an interface I.f
if C implements I.
Dynamic filtering
Filtering is supported through a standard method using a filter argument.
Specifically, if the field has an argument named filter with a type
that is an input object then the results of the materializer will be
filtered.
The fields of the filter input type must match the fields that are to be filtered.
type Employee {
email: String!
since: Date!
manager: ID
}
input EmployeeFilter {
email: StringEqFilter
since: DateRangeFilter
manager: ID # filter shorthand
}
extend type Query {
employees(filter:EmployeeFilter):[Employee] @materializer("_employees")
_employees: [Employee] ...
}The filter EmployeeFilter would operate on the email, since, and manager, on the data returned by _employees with the allowed predicate operators defined by the field's input type.
input StringEqFilter {
eq: String
}
input DateRangeFilter {
ge: Date!
lt: Date
}If the filter argument is not required and no argument value is provided or null, then no filtering is performed.
Multiple fields within the field argument filter input type and multiple operations in the allowable predicate operators
types are treated as conjunctions (with AND).
A value of filter:{email:{eq: "alice@example.com"} since:{ge:"2000-01-01"}} would result in execution of a dynamic filter that is the equivalent of email = filter.email AND since >= filter.since.
Field names and, or, not and nor are reserved for future logical operations.
The supported predicate operators are:
eq- equalne- not equalgt- greater thange- greater than or equallt- less thanle- less than or equalcontains- pattern match onStringfields.If the filter field pattern value starts with
/then the value must be a regular expression defined as/regex/optional-flags. The filter matches if the regex matches the contents of the field value. Flagiindicates case-insenstive matching.Otherwise, the filter matches if the characters within the pattern value are contained contiguously within the field value.
These predicate operators become field names in the allowable predicates input object type to enable specific predicates. The type of these fields must match the unwrapped type of the field being filtered.
The filtering may be delegated to the field set selection in query and not performed when materializing if:
filteris mapped as a argument to the field set selection inquerythe field set selection has an argument
filter; in this case,filteris mapped automatically
Limitation: Filtering only currently works for the top-level fields in the filtered object.
Filter shorthands
A shorthand syntax is supported for fields, where a field has a leaf type instead of an input object containing predicate opcodes.
Supported shorthand types are:
String,ID,Boolean- Equality, equivalent to an input object of{eq: T}whereTis the field type.Enum types - Equality, equivalent to an input object of
{eq: E}whereEis the field enum type.
Example: EmployeeFilter supports ID for manager.
Object reshaping
When applied to an object the selection in query must contain inline fragments with type conditions that indicate source
types and how source fields are mapped to the fields of the annotated type. Mappings are made through field
selections and use aliases for renaming.
This example indicates that the type DBCustomer can be mapped to Customer, renaming fields first_name and
last_name through aliases.
type DBCustomer {
id: ID!
first_name: String
last_name: String
}
type Customer
@materializer(
query: """
... on DBCustomer {
id
first: first_name
last: last_name
}
"""
) {
id: ID!
first: String
last: String
}An object is reshaped when a field @materializer annotates a field with a different object type to
the type of the materializing field. For example Query.customer will be reshaped from Query.dbcustomer:
type Query {
customer(id: ID!): Customer @materializer(query: "dbcustomer")
dbcustomer(id: ID!): DBCustomer
@dbquery(type: "mysql", table: "CUSTOMERS", configuration: "customerdb")
}Arguments
arguments: [StepZen_Argument!]
The arguments argument provides argument mapping for the materializer field set (query).
It provides values for the materializer field set from the following sources:
fields in the enclosing type, for example:
{name:"id" field:"author_id"}arguments of the field being materialized, for example:
{name:"unit" argument:"unit"}constants, for example
{name: "closed" const: false}
query: String!
The query argument specifies:
a field set selection that resolves the annotated field (location
FIELD_DEFINITION);a selection set with type conditions that indicate how source types are reshaped into the annotated type (location
OBJECT).
Field materialization
The syntax of query is a field set that selects a single field, with the root field being a field in Query. For example:
@materializer(query: "customer")- the annotated field is resolved by the execution of a query operationquery {customer {...}}(or equivalant).@materializer(query: "customer{address{city}}")- the annotated field is resolved by execution of a query operationquery {customer{address{city}}}(or equivalant) and extracting thecityfield value.
The selected field can be either a simple value (leaf) or an object (composite). When using a composite type, any sub-fields selected against the annotated field are automatically included in the query operation.
The type of the selected field (source type) must be assignable to the type of the annotated field (target type).
Scalar types are assignable if the source type can be coerced to the target type, for example
Int!toInt, orInt!toID!.Composite types are assignable if:
a source type is assigned to an interface target type it implements.
a source type is assigned to a union target type it is a member of.
Arguments of the target selection are explicitly mapped according to arguments.
Field arguments not explicitly mapped by arguments are implicitly mapped:
from a field argument of the same argument name if the annotated field is in root operation type
Query.from a field with the same name as the argument name if the annnotated field is in a non-root type. This type is the enclosing type of the annotated field.
All required arguments of the target selection must be explicitly or implicitly mapped.
Optional arguments of the target selection take their default value if defined, otherwise null.
A nullable field or argument can be mapped into a non-nullable field argument. At runtime if the value is null then:
for a required argument (no default value) the call to the materializing selection is not made and the annotated field returns
null.for an optional field argument
nullis replaced by the default value of the target argument.
Object reshaping
The selection set must contain inline fragments with type conditions that correspond to source types that can be mapped to the annotated type.
The type conditions can be on any valid type, so that for example Character here is an interface:
type Robot
@materializer(
query: """
... on Character {
id
handle:name
}
... on Droid {
purpose: primaryFunction
}
"""
) {
id: ID!
handle: String
purpose: String
}The selection under a type condition, can also include type conditions, for example the above selection could be rewritten as:
... on Character {
id
handle:name
... on Droid {
purpose: primaryFunction
}
}Any number of inline fragments with type conditions can exist to support multiple source types, at runtime the GraphQL engine extracts the required fragments for the source type.
Locations
Type System: FIELD_DEFINITION, OBJECT
Field definition examples
Adding relationships to your graph
With GraphQL, you model your business domain as a graph.
The @materializer custom directive allows you to add relationships into your schema's graphql by adding fields to types.
This example shows how @materializer adds to a customer object type to include their orders from a different backend system.
When Customer.orders is selected, it will be resolved by Query.orders.
In this case argument mapping is implicit, with @materializer the argument Query.orders(id:) defaults
to a field of the same name and type in the enclosing type (Customer).
type Query {
customer(id: ID!): Customer @dbquery(type: "postgresql", table: "customers")
orders(id: ID!): [Order]
@graphql(endpoint: "$url", configuration: "orders-system")
}
type Customer {
id: ID!
name: String
email: String
orders: [Order] @materializer(query: "orders")
}Now a request can be made such as:
query Customer($id: ID!) {
customer(id: $id) {
name
email
orders {
id
cost
delivered
}
}
}In this case, the single GraphQL incoming request will result in two outgoing requests, to the database and backend GraphQL systems, but the client making the original GraphQL request is unaware of how the response is created.
Note that the materializing field Query.orders can be resolved by any means, including @connector, @dbquery, @documentstore, @graphql, @materializer, @rest, @sequence and @value, or field that is resolved through @supplies.
Using extend type
A typical pattern is not to add fields to the original type (Customer) definition, but instead use extend type.
This would leave the original Customer defintions as:
type Customer {
id: ID!
name: String
email: String
}and separately extend it:
extend type Customer {
orders: [Order] @materializer(query: "orders")
}This separation:
Works when the original definition cannot be modified (for example, pulled from a separate code repository).
Allows deployment of endpoints with different combination of extensions (or none).
For example, endpoints with:
just customer information
customer and orders
customer and loyalty program information
Allows unit testing of schema elements.
Allows source code locality for an extension and its related schema elements.
Argument mapping
The directive argument @materializer(arguments:) allows mapping to field arguments of the materializing field from:
fields of the enclosing type of the materialized field
field arguments of the materializing field
constants
Any required argument of the materializing field, must be set by @materializer, either through implicit or explicit (@materializer(arguments:)) mapping.
Name mapping
The argument name customerId is different to the field name id from Customer.
extend type Query {
orders(customerId: ID!): [Order]
@graphql(endpoint: "$url", configuration: "orders-system")
}
extend type Customer {
orders: [Order] @materializer(query: "orders" arguments: {name:"customerId" field:"id})
}Input object fields
Query.orders has a filter argument, such that it can select orders as needed, and is not restricted to a single customer.
input OrdersFilter {
customerId: ID
delivery: DateTimeFilter
cost: CostFilter
carrier: CarrierFilter
open: Boolean
}
extend type Query {
orders(filter: OrdersFilter): [Order]
@graphql(endpoint: "$url", configuration: "orders-system")
}To extend Customer we need to ensure a filter argument is passed that restricts the request to that customer's orders,
a JSON dotted path is used to specify fields within the input object.
extend type Customer {
orders: [Order]
@materializer(
query: "orders"
arguments: {name: "filter.customerId", field: "id"}
)
}Field arguments
The previous example can be modified to allow the request to specify open or closed orders and which carrier,
by adding field arguments to the materialized field Customer.orders.
extend type Customer {
orders(open: Boolean, carrier: String): [Order]
@materializer(
query: "orders"
arguments: [
{name: "filter.customerId", field: "id"}
{name: "filter.open", argument: "open"}
{name: "filter.carrier.eq", argument: "carrier"}
]
)
}Constants
The previous example can be modified to restrict which orders are returned, by using constants.
In this case, orders are restricted to open orders, such as in a endpoint solely meant for customers tracking their orders.
extend type Customer {
orders(carrier: String): [Order]
@materializer(
query: "orders"
arguments: [
{name: "filter.customerId", field: "id"}
{name: "filter.carrier.eq", argument: "carrier"}
{name: "filter.open", const: true}
]
)
}Renaming Query fields
@materializer can be used with fields in the root operation type Query, one use is simple renaming of fields.
You may want to rename a field name or argument name when an existing name is unclear or does not match your naming convention.
Here Query.customerOrders effectively acts as a renaming of Query.orders_for_customer.
type Query {
orders_for_customer(id: ID!): [Order]
@dbquery(type: "postgresql", table: "orders")
customerOrders(id: ID!): [Order] @materializer(query: "orders_for_customer")
}When annotating fields in Query, field argments of the materializing field are mapped implicitly
from arguments of the annotated (materialized) field. Explicit mapping is supported from
arguments of the annotated field or constants.
To create a clean schema, materializing fields can be hidden through field visibility patterns using directive
argument@sdl(visibility:), or by field access rules.
Constraining field arguments
A common use for fields in Query is to create a field materialized by another field but constrain the
arguments available to GraphQL requests.
For example, Query.openOrders is materialized by Query.orders but does not allow selecting all or just closed orders:
type Query {
orders(id: ID!, open: Boolean = true): [Order]
@dbquery(type: "postgresql", table: "orders")
openOrders(id: ID!): [Order] @materializer(query: "orders_for_customer")
}This example works because Query.orders(open:) is not required, argument mapping can
be used with constants to provided values for required arguments
or different values for optional arguments.
In this case, Query.orders would be hidden using field access rules or visibility.
With field access rules, accessible fields could be based upon a role in a JWT,
so a customer could see all their orders through Query.orders, while a customer support representive
could only see open orders through Query.openOrders.