8.5 KiB
title | summary |
---|---|
The DataObject model type | An overview of how the DataObject model can influence the creation of types, queries, and mutations |
Working with DataObjects
[CHILDREN asList]
[alert] You are viewing docs for a pre-release version of silverstripe/graphql (4.x). Help us improve it by joining #graphql on the Community Slack, and report any issues at github.com/silverstripe/silverstripe-graphql. Docs for the current stable version (3.x) can be found here [/alert]
The DataObject model type
In Silverstripe CMS projects, our data tends to be contained in dataobjects almost exclusively, and the silverstripe-graphql schema API is designed to make adding dataobject content fast and simple.
Using model types
While it is possible to add dataobjects to your schema as generic types under the types
section of the configuration, and their associated queries and mutations under queries
and
mutations
, this will lead to a lot of boilerplate code and repetition. Unless you have some
really custom needs, a much better approach is to embrace convention over configuration
and use the models
section of the config.
Model types are types that rely on external classes to tell them who they are and what they can and cannot do. The model can define and resolve fields, auto-generate queries and mutations, and more.
Naturally, this module comes bundled with a model type for subclasses of DataObject
.
Let's use the models
config to expose some content.
app/_graphql/models.yml
Page:
fields: '*'
operations: '*'
The class Page
is a subclass of DataObject
, so the bundled model
type will kick in here and provide a lot of assistance in building out this part of our API.
Case in point, by supplying a value of *
for fields
, we're saying that we want all of the fields
on site tree. This includes the first level of relationships, as well, as defined on has_one
, has_many
,
or many_many
.
[notice]
Fields on relationships will not inherit the *
fields selector, and will only expose their ID by default.
[/notice]
The *
value on operations
tells the schema to create all available queries and mutations
for the dataobject, including:
read
readOne
create
update
delete
Now that we've changed our schema, we need to build it using the build-schema
task:
$ vendor/bin/sake dev/graphql/build schema=default
Now, we can access our schema on the default graphql endpoint, /graphql
.
Test it out!
A query:
query {
readPages {
nodes {
title
}
}
A mutation:
mutation {
createPage(input: {
title: "my page"
}) {
title
id
}
}
[info] Did you get a permissions error? Make sure you're authenticated as someone with appropriate access. [/info]
Configuring operations
You may not always want to add all operations with the *
wildcard. You can allow those you
want by setting them to true
(or false
to remove them).
app/_graphql/models.yml
Page:
fields: '*'
operations:
read: true
create: true
Operations are also configurable, and accept a nested map of config.
app/_graphql/models.yml
Page:
fields: '*'
operations:
create: true
read:
name: getAllThePages
Customising the input types
The input types, specifically in create
and update
can be customised with a whitelist
and/or blacklist of fields.
app/_graphql/models.yml
Page:
fields: '*'
operations:
create:
fields:
title: true
content: true
update:
exclude:
sensitiveField: true
Adding more fields
Let's add some more dataobjects, but this time, we'll only add a subset of fields and operations.
app/_graphql/models.yml
Page:
fields: '*'
operations: '*'
MyProject\Models\Product:
fields:
onSale: true
title: true
price: true
operations:
delete: true
MyProject\Models\ProductCategory:
fields:
title: true
featured: true
[notice] A couple things to note here:
- By assigning a value of
true
to the field, we defer to the model to infer the type for the field. To override that, we can always add atype
property:
onSale:
type: Boolean
- The mapping of our field names to the DataObject property is case-insensitive. It is a convention in GraphQL APIs to use lowerCamelCase fields, so this is given by default. [/notice]
Customising model fields
You don't have to rely on the model to tell you how fields should resolve. Just like generic types, you can customise them with arguments and resolvers.
app/_graphql/models.yml
MyProject\Models\Product:
fields:
title:
type: String
resolver: [ 'MyProject\Resolver', 'resolveSpecialTitle' ]
'price(currency: String = "NZD")': true
For more information on custom arguments and resolvers, see the adding arguments and resolver discovery documentation.
Excluding or customising "*" declarations
You can use the *
as a field or operation, and anything that follows it will override the
all-inclusive collection. This is almost like a spread operator in Javascript:
const newObj = {...oldObj, someProperty: 'custom' }
Here's an example:
app/_graphql/models.yml
Page:
fields:
'*': true # Get everything
sensitiveData: false # hide this field
'content(summaryLength: Int)': true # add an argument to this field
operations:
'*': true
read:
plugins:
paginateList: false # don't paginate the read operation
Blacklisted fields
While selecting all fields via *
is usedful, there are some fields that you
don't want to accidentally expose, especially if you're a module author
and expect models within this code to be used through custom GraphQL endpoints.
For example, a module might add a secret "preview token" to each SiteTree
.
A custom GraphQL endpoint might have used fields: '*'
on SiteTree
to list pages
on the public site, which now includes a sensitive field.
The graphql_blacklisted_fields
property on DataObject
allows you to
blacklist fields globally for all GraphQL schemas.
This blacklist applies for all operations (read, update, etc).
app/_config/graphql.yml
SilverStripe\CMS\Model\SiteTree:
graphql_blacklisted_fields:
myPreviewTokenField: true
Model configuration
There are several settings you can apply to your model class (typically DataObjectModel
),
but because they can have distinct values per schema, the standard _config
layer is not
an option. Model configuration has to be done within the schema config in the modelConfig
subsection.
Customising the type name
Most DataObject classes are namespaced, so converting them to a type name ends up
being very verbose. As a default, the DataObjectModel
class will use the "short name"
of your DataObject as its typename (see: ClassInfo::shortName()
). That is,
MyProject\Models\Product
becomes Product
.
Given the brevity of these type names, it's not inconceivable that you could run into naming collisions, particularly if you use feature-based namespacing. Fortunately, there are hooks you have available to help influence the typename.
The type formatter
The type_formatter
is a callable that can be set on the DataObjectModel
config. It takes
the $className
as a parameter.
Let's turn MyProject\Models\Product
into the more specific MyProjectProduct
app/_graphql/config.yml
modelConfig:
DataObject:
type_formatter: ['MyProject\Formatters', 'formatType' ]
[info]
In the above example, DataObject
is the result of the DataObjectModel::getIdentifier()
. Each
model class must declare one of these.
[/info]
Your formatting function could look something like:
public static function formatType(string $className): string
{
$parts = explode('\\', $className);
if (count($parts) === 1) {
return $className;
}
$first = reset($parts);
$last = end($parts);
return $first . $last;
}
The type prefix
You can also add prefixes to all your DataObject types. This can be a scalar value or a callable,
using the same signature as type_formatter
.
app/_graphql/config.yml
modelConfig:
DataObject
type_prefix: 'MyProject'
Further reading
[CHILDREN]