Working with DataObject
models
- DataObject query plugins
- Learn about some of the useful goodies that come pre-packaged with DataObject queries
- DataObject operation permissions
- A look at how permissions work for DataObject queries and mutations
- DataObject inheritance
- Learn how inheritance is handled in DataObject model types
- Adding DataObject models to the schema
- An overview of how the DataObject model can influence the creation of types, queries, and mutations
- Versioned content
- A guide on how DataObject models with the Versioned extension behave in GraphQL schemas
- Nested type definitions
- Define dependent types inline with a parent type
- Property mapping and dot syntax
- Learn how to customise field names, use dot syntax, and use aggregate functions
DataObject
query plugins
This module has a plugin system that affords extensibility to queries, mutations,
types, fields, and just about every other thread of the schema. Model types can define default
plugins to include, and for DataObject
queries, these include:
filter
sort
dbFieldArgs
paginateList
inheritance
canView
(read, readOne)firstResult
(readOne)
When the silverstripe/cms
module is installed, a plugin known as getByLink
is also added.
Other modules, such as silverstripe/versioned
may augment that list with even more.
The pagination plugin
The pagination plugin augments your queries in two main ways:
- Adding
limit
andoffset
arguments -
Wrapping the return type in a "connection" type with the following fields:
nodes: '[YourType]'
edges: '[{ node: YourType }]'
pageInfo: '{ hasNextPage: Boolean, hasPreviousPage: Boolean: totalCount: Int }'
Let's test it out:
query {
readPages(limit: 10, offset: 20) {
nodes {
title
}
edges {
node {
title
}
}
pageInfo {
totalCount
hasNextPage
hasPrevPage
}
}
}
If you're not familiar with the jargon of edges
and node
, don't worry too much about it
for now. It's just a pretty well-established convention for pagination in GraphQL, mostly owing
to its frequent use with cursor-based pagination, which
isn't something we do in Silverstripe CMS. You can ignore edges.node
and just use nodes
if
you want to.
Limiting pagination
To change the limit for items per page for a given type, you can set the maximumLimit
property on the paginateList
plugin in the schema:
# app/_graphql/models.yml
MyProject\Models\ProductCategory:
operations:
read:
plugins:
paginateList:
maximumLimit: 10
To change the default limit globally, set the max_limit configuration on the PaginationPlugin
itself:
SilverStripe\GraphQL\Schema\Plugin\PaginationPlugin:
max_limit: 10
If you want to increase the limit beyond the default value, you will also need to set a new default_limit
configuration value on the PaginationPlugin
.
Disabling pagination
Just set it to false
in the configuration.
# app/_graphql/models.yml
App\Model\ProductCategory:
operations:
read:
plugins:
paginateList: false
To disable pagination globally, use modelConfig
:
# app/_graphql/config.yml
modelConfig:
DataObject:
operations:
read:
plugins:
paginateList: false
The filter plugin
The filter plugin (QueryFilter
) adds a
special filter
argument to the read
and readOne
operations.
query {
readPages(
filter: { title: { eq: "Blog" } }
) {
nodes {
title
created
}
}
}
In the above example, the eq
is known as a "comparator". There are several of these
included with the the module, including:
eq
(exact match)ne
(not equal)contains
(fuzzy match)gt
(greater than)lt
(less than)gte
(greater than or equal)lte
(less than or equal)in
(in a given list)startswith
(starts with)endswith
(ends with)
Example:
query {
readPages (
filter: {
title: { ne: "Home" },
created: { gt: "2020-06-01", lte: "2020-09-01" }
}
) {
nodes {
title
created
}
}
}
While it is possible to filter using multiple comparators, segmenting them into disjunctive groups (e.g. "OR" and "AND" clauses) is not yet supported.
Nested fields are supported by default:
query {
readProductCategories(
filter: {
products: {
reviews: {
rating: { gt: 3 },
comment: { contains: "awesome" },
author: { ne: "Me" }
}
}
}
) {
nodes {
title
}
}
}
Filters are only querying against the database by default - it is not possible to filter by fields with custom resolvers.
Customising the filter fields
By default, all fields on the DataObject, including relationships, are included. To customise
this, just add a fields
config to the plugin definition:
# app/_graphql/models.yml
App\Model\ProductCategory:
fields:
title: true
featured: true
operations:
read:
plugins:
filter:
fields:
title: true
You can also add all fields with '*': true
, just like with standard model definitions.
Adding non-native filter fields
Sometimes you may want to add a filter field that stems from a custom getter, or a complex computation that isn't easily addressed by simple field comparisons. For cases like this, you can add the custom field as long as you provide instructions on how to resolve it.
# app/_graphql/models.yml
App\Model\Product:
fields:
title: true
price: true
operations:
read:
plugins:
filter:
fields:
title: true
hasReviews: true
resolve:
hasReviews:
type: Boolean
resolver: ['App\GraphQL\Resolver\ProductResolver', 'resolveHasReviewsFilter']
We've added the custom field hasReviews
as a custom field in the fields
section of the plugin config. A custom field
like this that does not exist on the Product
dataobject will cause the plugin to throw unless you've provided
a resolve
directive for it.
In the resolve
section, we need to provide two vital pieces of information:
- What data type will the filter value be? (boolean in this case)
- Where is the code that will apply this filter? (A static function in our
ProductResolver
class)
The code to resolve the filter will get two relevant pieces of information in its $context
parameter:
filterComparator
: e.g. "eq", "ne", "gt", etc.filterValue
: What value we're comparing (true or false, in this case, since it's a boolean)
Here's how we can resolve this custom filter:
// app/src/GraphQL/Resolver/Resolver.php
namespace App\GraphQL\Resolver;
use Exception;
class ProductResolver
{
public static function resolveHasReviewsFilter(Filterable $list, array $args, array $context)
{
$onlyWithReviews = $context['filterValue'];
$comparator = $context['filterComparator'];
if (!in_array($comparator, ['eq', 'ne'])) {
throw new Exception('Invalid comparator for hasReviews: ' . $comparator);
}
if ($comparator === 'ne') {
$onlyWithReviews = !$onlyWithReviews;
}
return $onlyWithReviews
? $list->filter('Reviews.Count():GreaterThan', 0)
: $list->filter('Reviews.Count()', 0);
}
}
Custom filter fields are also a good opportunity to implement something like filterByCallback
on your list for
particularly complex computations that cannot be done at the database level.
Disabling the filter plugin
Just set it to false
in the configuration.
# app/_graphql/models.yml
App\Model\ProductCategory:
operations:
read:
plugins:
filter: false
To disable filtering globally, use modelConfig
:
# app/_graphql/config.yml
modelConfig:
DataObject:
operations:
read:
plugins:
filter: false
The sort plugins
The sort plugin (QuerySort
) adds a
special sort
argument to the read
and readOne
operations.
query {
readPages (
sort: { created: DESC }
) {
nodes {
title
created
}
}
}
Nested fields are supported by default, but only for linear relationships (e.g. has_one
):
query {
readProducts(
sort: {
primaryCategory: {
lastEdited: DESC
}
}
) {
nodes {
title
}
}
}
In addition, you can use the field sorting plugin (SortPlugin
) to sort fields that represent has_many
and many_many
relationships. To do this, simply add the desired fields to the query, as well as the sort
argument to these fields. It is also necessary to update the scheme by adding a sorter
plugin to those fields that need to be sorted.
Example how to use SortPlugin.
query {
readPages (
sort: { created: DESC }
) {
nodes {
title
created
hasManyFilesField (sort: { parentFolderID: DESC, title: ASC }) {
name
}
}
}
}
# app/_graphql/models.yml
Page:
operations:
read:
plugins:
sort:
before: paginateList
fields:
created: true
fields:
title: true
created: true
hasManyFilesField:
fields:
name: true
plugins:
sorter:
fields:
title: true
parentFolderID: true
Customising the sort fields
By default, all fields on the DataObject, including has_one
relationships, are included.
To customise this, just add a fields
config to the plugin definition:
# app/_graphql/models.yml
App\Model\ProductCategory:
fields:
title: true
featured: true
operations:
read:
plugins:
sort:
fields:
title: true
Disabling the sort plugin
Just set it to false
in the configuration.
# app/_graphql/models.yml
App\Model\ProductCategory:
operations:
read:
plugins:
sort: false
To disable sort globally, use modelConfig
:
# app/_graphql/config.yml
modelConfig:
DataObject:
operations:
read:
plugins:
sort: false
The DBFieldArgs
plugin
When fields are introspected from a model and reference a DBField
instance,
they get populated with a default set of arguments that map to methods on that
DBField
class, for instance $field->Nice()
or $field->LimitSentences(4)
.
Let's have a look at this query:
query {
readPages {
nodes {
content(format: LIMIT_SENTENCES, limit: 4)
created(format: NICE)
... on BlogPage {
introText(format: FIRST_PARAGRAPH)
publishDate(format: CUSTOM, customFormat: "dd/MM/yyyy")
}
}
}
}
The primary field types that are affected by this include:
DBText
(includingDBHTMLText
)DBDate
(includingDBDatetime
)DBTime
DBDecimal
DBFloat
All available arguments
DBText
format: CONTEXT_SUMMARY
(optional "limit" arg)format: FIRST_PARAGRAPH
format: LIMIT_SENTENCES
(optional "limit" arg)format: SUMMARY
(optional "limit" arg)parseShortcodes: Boolean
(DBHTMLText only)
DBDate
format: TIMESTAMP
format: NICE
format: DAY_OF_WEEK
format: MONTH
format: YEAR
format: SHORT_MONTH
format: DAY_OF_MONTH
format: SHORT
format: LONG
format: FULL
format: CUSTOM
(requirescustomFormat: String
arg)
DBTime
format: TIMESTAMP
format: NICE
format: SHORT
format: CUSTOM
(requirescustomFormat: String
arg)
DBDecimal
format: INT
DBFloat
format: NICE
format: ROUND
format: NICE_ROUND
Enum naming strategy and deduplication
By default, auto-generated Enum types will use as generic a name as possible using the convention <FieldName>Enum
(e.g.
OrderStatusEnum
). On occasion, this may collide with other types (e.g. OptionsEnum
is quite generic and likely to be used already).
In this case, the second enum generated will use <TypeName><FieldName>Enum
(e.g. MyTypeOptionsEnum
).
If an enum already exists with the same fields and name, it will be reused. For instance, if OptionsEnum
is found and has exactly the same defined values (in the same order) as the Enum being generated,
it will be reused rather than proceeding to the deduplication strategy.
Custom enum names
You can specify custom enum names in the plugin config:
# app/_graphql/config.yml
modelConfig:
DataObject:
plugins:
dbFieldTypes:
enumTypeMapping:
MyType:
myEnumField: SomeCustomTypeName
You can also specify enums to be ignored. (ClassName
does this on all DataObjects to prevent inheritance
issues)
# app/_graphql/config.yml
modelConfig:
DataObject:
plugins:
dbFieldTypes:
ignore:
MyType:
myEnumField: true
The getByLink plugin
When the silverstripe/cms
module is installed (it is in most cases), a plugin called getByLink
will ensure that queries that return a single DataObject
model (e.g. readOne
) get a new query argument
called link
(configurable on the field_name
property of LinkablePlugin
).
readOneSiteTree(link: "/about-us" ) {
title
}