mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-09-18 23:46:21 +02:00
311 lines
8.5 KiB
Markdown
311 lines
8.5 KiB
Markdown
---
|
|
title: The DataObject model type
|
|
summary: 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](https://www.silverstripe.org/blog/community-slack-channel/),
|
|
and report any issues at [github.com/silverstripe/silverstripe-graphql](https://github.com/silverstripe/silverstripe-graphql).
|
|
Docs for the current stable version (3.x) can be found
|
|
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
|
|
[/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:
|
|
```graphql
|
|
query {
|
|
readPages {
|
|
nodes {
|
|
title
|
|
}
|
|
}
|
|
```
|
|
|
|
A mutation:
|
|
```graphql
|
|
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](#blacklisted-fields) 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*
|
|
```yaml
|
|
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 a `type` property:
|
|
|
|
```yaml
|
|
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*
|
|
```yaml
|
|
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](../working_with_generic_types/adding_arguments) and [resolver discovery](../working_with_generic_types/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:
|
|
|
|
```js
|
|
const newObj = {...oldObj, someProperty: 'custom' }
|
|
```
|
|
|
|
Here's an example:
|
|
|
|
**app/_graphql/models.yml**
|
|
```yaml
|
|
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 {#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**
|
|
```yaml
|
|
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*
|
|
```yaml
|
|
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:
|
|
|
|
```php
|
|
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*
|
|
```yaml
|
|
modelConfig:
|
|
DataObject
|
|
type_prefix: 'MyProject'
|
|
```
|
|
|
|
### Further reading
|
|
|
|
[CHILDREN]
|