mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
WIP: Add new graphql 4 docs (#9652)
* DOCS: Add new graphql 4 docs * Reorganise docs * Docs done * Basic graphql index page * TOC for getting started * show folders on graphql index page * Add middleware note * Docs update * Update docs to reflect flushless schema * Docs updates * Docs for getByLink * Query caching docs * Docs on nested operations * update docs for new graphql dev admin * Docs for configurable operations * Replace readSiteTrees with readPages * Schema defaults docs * Docs for inherited plugins * Docs for customising * * Docs for field whitelisting * Change whitelist word * New docs on modelConfig * Document dev/build extension * Document default/global plugins * Document new input type fields config * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * Note about when procedural schema gets built * Fix link * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * DOCS Note about plugins in custom queries * DOCS Note about filter and custom resolvers * DOCS Note about canview paging * DOCS Updated guidance on _extend See https://github.com/silverstripe/silverstripe-graphql/issues/296 * Apply suggestions from code review Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> * DOCS Pre-release warning Co-authored-by: Ingo Schommer <ingo@silverstripe.com> Co-authored-by: Andre Kiste <bergice@users.noreply.github.com> Co-authored-by: Ingo Schommer <me@chillu.com>
This commit is contained in:
parent
4670cd3af9
commit
c1cda2b113
@ -1,159 +0,0 @@
|
|||||||
---
|
|
||||||
title: Controlling CanView access to DataObjects returned by GraphQL
|
|
||||||
summary: Your GraphQL service should honour the CanView permission when fetching DataObjects. Learn how to customise this access control check.
|
|
||||||
icon: cookie-bite
|
|
||||||
---
|
|
||||||
|
|
||||||
# Controlling who can view results in a GraphQL result set
|
|
||||||
|
|
||||||
The [Silverstripe ORM provides methods to control permissions on DataObject](Developer_Guides/Model/Permissions). In
|
|
||||||
most cases, you'll want to extend this permission model to any GraphQL service you implement that returns a DataObject.
|
|
||||||
|
|
||||||
## The QueryPermissionChecker interface
|
|
||||||
|
|
||||||
The GraphQL module includes a `QueryPermissionChecker` interface. This interface can be used to specify how GraphQL
|
|
||||||
services should validate that users have access to the DataObjects they are requesting.
|
|
||||||
|
|
||||||
The default implementation of `QueryPermissionChecker` is `CanViewPermissionChecker`. `CanViewPermissionChecker` directly calls the `CanView` method of each DataObject in your result set and filters out the entries not visible to the current user.
|
|
||||||
|
|
||||||
Out of the box, the `CanView` permission of your DataObjects are honoured when Scaffolding GraphQL queries.
|
|
||||||
|
|
||||||
## Customising how the results are filtered
|
|
||||||
|
|
||||||
`CanViewPermissionChecker` has some limitations. It's rather simplistic and will load each entry in your results set to perform a CanView call on it. It will also convert the results set to an `ArrayList` which can be inconvenient if you need to alter the underlying query after the _CanView_ check.
|
|
||||||
|
|
||||||
Depending on your exact use case, you may want to implement your own `QueryPermissionChecker` instead of relying
|
|
||||||
on `CanViewPermissionChecker`.
|
|
||||||
|
|
||||||
Some of the reasons you might consider this are:
|
|
||||||
* the access permissions on your GraphQL service differ from the ones implemented directly on your DataObject
|
|
||||||
* you want to speed up your request by filtering out results the user doesn't have access to directly in the query
|
|
||||||
* you would rather have a `DataList` be returned.
|
|
||||||
|
|
||||||
### Implementing your own QueryPermissionChecker class
|
|
||||||
|
|
||||||
The `QueryPermissionChecker` requires your class to implement two methods:
|
|
||||||
* `applyToList` which filters a `Filterable` list based on whether a provided `Member` can view the results
|
|
||||||
* `checkItem` which checks if a provided object can be viewed by a specific `Member`.
|
|
||||||
|
|
||||||
#### Filtering results based on the user's permissions
|
|
||||||
|
|
||||||
In some context, whether a user can view an object is entirely determined on their permissions. When that's the
|
|
||||||
case, you don't even need to get results to know if the user will be able to see them or not.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use SilverStripe\GraphQL\Permission\QueryPermissionChecker;
|
|
||||||
use SilverStripe\ORM\ArrayList;
|
|
||||||
use SilverStripe\ORM\Filterable;
|
|
||||||
use SilverStripe\Security\Member;
|
|
||||||
use SilverStripe\Security\Permission;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This implementation assumes that only users with the ADMIN permission can view results.
|
|
||||||
*/
|
|
||||||
class AdminPermissionChecker implements QueryPermissionChecker
|
|
||||||
{
|
|
||||||
|
|
||||||
public function applyToList(Filterable $list, Member $member = null)
|
|
||||||
{
|
|
||||||
return Permission::check('ADMIN', 'any', $member) ?
|
|
||||||
$list :
|
|
||||||
ArrayList::create([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkItem($item, Member $member = null)
|
|
||||||
{
|
|
||||||
return Permission::check('ADMIN', 'any', $member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Filtering results based on the user's permissions
|
|
||||||
|
|
||||||
Some times, whether a user can view an object is determined by some information on the record. If that's the case,
|
|
||||||
you can filter out results the user can not see by altering the query. This has some performance advantage, because
|
|
||||||
the results are filtered directly by the query.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
use SilverStripe\GraphQL\Permission\QueryPermissionChecker;
|
|
||||||
use SilverStripe\ORM\Filterable;
|
|
||||||
use SilverStripe\Security\Member;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This implementation assumes that the results are assigned an owner and that only the owner can view a record.
|
|
||||||
*/
|
|
||||||
class OwnerPermissionChecker implements QueryPermissionChecker
|
|
||||||
{
|
|
||||||
|
|
||||||
public function applyToList(Filterable $list, Member $member = null)
|
|
||||||
{
|
|
||||||
return $list->filter('OwnerID', $member ? $member->ID : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkItem($item, Member $member = null)
|
|
||||||
{
|
|
||||||
return $member && $item->OwnerID === $member->ID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using a custom QueryPermissionChecker implementation
|
|
||||||
|
|
||||||
There's three classes that expect a `QueryPermissionChecker`:
|
|
||||||
* `SilverStripe\GraphQL\Scaffolding\Scaffolders\ItemQueryScaffolder`
|
|
||||||
* `SilverStripe\GraphQL\Scaffolding\Scaffolders\ListQueryScaffolder`
|
|
||||||
* `SilverStripe\GraphQL\Pagination\Connection`
|
|
||||||
|
|
||||||
Those classes all implement the `SilverStripe\GraphQL\Permission\PermissionCheckerAware` and receive the default
|
|
||||||
`QueryPermissionChecker` from the Injector. They also have a `setPermissionChecker` method that can be use to provide a custom `QueryPermissionChecker`.
|
|
||||||
|
|
||||||
#### Scaffolding types with a custom QueryPermissionChecker implementation
|
|
||||||
|
|
||||||
There's not any elegant way of defining a custom `QueryPermissionChecker` when scaffolding types at this time. If
|
|
||||||
you need the ability to use a custom `QueryPermissionChecker`, you'll have to build your query manually.
|
|
||||||
|
|
||||||
#### Overriding the QueryPermissionChecker for a class extending ListQueryScaffolder
|
|
||||||
|
|
||||||
If you've created a GraphQL query by creating a subclass of `ListQueryScaffolder`, you can use the injector to
|
|
||||||
override `QueryPermissionChecker`.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
Name: custom-graphqlconfig
|
|
||||||
After: graphqlconfig
|
|
||||||
---
|
|
||||||
SilverStripe\Core\Injector\Injector:
|
|
||||||
SilverStripe\GraphQL\Permission\QueryPermissionChecker.my-custom:
|
|
||||||
class: App\Project\CustomQueryPermissionChecker
|
|
||||||
App\Project\CustomListQueryScaffolder:
|
|
||||||
properties:
|
|
||||||
permissionChecker: '%$SilverStripe\GraphQL\Permission\QueryPermissionChecker.my-custom'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Manually specifying a QueryPermissionChecker on a Connection
|
|
||||||
|
|
||||||
If you're manually instantiating an instance of `SilverStripe\GraphQL\Pagination\Connection` to resolve your results,
|
|
||||||
you can pass an instance of your own custom `QueryPermissionChecker`.
|
|
||||||
|
|
||||||
```php
|
|
||||||
$childrenConnection = Connection::create('Children')
|
|
||||||
->setConnectionType($this->manager->getType('Children'))
|
|
||||||
->setSortableFields([
|
|
||||||
'id' => 'ID',
|
|
||||||
'title' => 'Title',
|
|
||||||
'created' => 'Created',
|
|
||||||
'lastEdited' => 'LastEdited',
|
|
||||||
])
|
|
||||||
->setPermissionChecker(new CustomQueryPermissionChecker());
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Documentation
|
|
||||||
|
|
||||||
* [CanViewPermissionChecker](api:SilverStripe\GraphQL\Permission\CanViewPermissionChecker)
|
|
||||||
* [Connection](api:SilverStripe\GraphQL\Pagination\Connection)
|
|
||||||
* [ItemQueryScaffolder](api:SilverStripe\GraphQL\Scaffolding\Scaffolders\ItemQueryScaffolder)
|
|
||||||
* [ListQueryScaffolder](api:SilverStripe\GraphQL\Scaffolding\Scaffolders\ListQueryScaffolder)
|
|
||||||
* [PermissionCheckerAware](api:SilverStripe\GraphQL\Permission\PermissionCheckerAware)
|
|
||||||
* [QueryPermissionChecker](api:SilverStripe\GraphQL\Permission\QueryPermissionChecker)
|
|
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: Activating the default server
|
||||||
|
summary: Open up the default server that comes pre-configured with the module
|
||||||
|
icon: rocket
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Activating the default GraphQL server
|
||||||
|
|
||||||
|
GraphQL is used through a single route, typically `/graphql`. You need
|
||||||
|
to define *types* and *queries* to expose your data via this endpoint. While this recommended
|
||||||
|
route is left open for you to configure on your own, the modules contained in the [CMS recipe](https://github.com/silverstripe/recipe-cms),
|
||||||
|
(e.g. `asset-admin`) run off a separate GraphQL server with its own endpoint
|
||||||
|
(`admin/graphql`) with its own permissions and schema.
|
||||||
|
|
||||||
|
These separate endpoints have their own identifiers. `default` refers to the GraphQL server
|
||||||
|
in the user space (e.g. `/graphql`) while `admin` refers to the GraphQL server used by CMS modules
|
||||||
|
(`admin/graphql`). You can also [set up a new schema](#setting-up-a-custom-graphql-server) if you wish.
|
||||||
|
|
||||||
|
By default, this module does not route any GraphQL servers. To activate the default,
|
||||||
|
public-facing GraphQL server that ships with the module, just add a rule to `Director`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Control\Director:
|
||||||
|
rules:
|
||||||
|
'graphql': '%$SilverStripe\GraphQL\Controller.default'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up a custom GraphQL server
|
||||||
|
|
||||||
|
In addition to the default `/graphql` endpoint provided by this module by default,
|
||||||
|
along with the `admin/graphql` endpoint provided by the CMS modules (if they're installed),
|
||||||
|
you may want to set up another GraphQL server running on the same installation of SilverStripe.
|
||||||
|
|
||||||
|
Let's set up a new controller to handle the requests.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
# ...
|
||||||
|
SilverStripe\GraphQL\Controller.myNewSchema:
|
||||||
|
class: SilverStripe\GraphQL\Controller
|
||||||
|
constructor:
|
||||||
|
schemaKey: myNewSchema
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
We'll now need to route the controller.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Control\Director:
|
||||||
|
rules:
|
||||||
|
'my-graphql': '%$SilverStripe\GraphQL\Controller.myNewSchema'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you're ready to [configure your schema](configuring_your_schema.md).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
myNewSchema:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
title: Configuring your schema
|
||||||
|
summary: Add a basic type to the schema configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Configuring your schema
|
||||||
|
|
||||||
|
GraphQL is a strongly-typed API layer, so having a schema behind it is essential. Simply put:
|
||||||
|
|
||||||
|
* A schema consists of **[types](https://graphql.org/learn/schema/#type-system)**
|
||||||
|
* **Types** consist of **[fields](https://graphql.org/learn/queries/#fields)**
|
||||||
|
* **Fields** can have **[arguments](https://graphql.org/learn/queries/#arguments)**.
|
||||||
|
* **Fields** need to **[resolve](https://graphql.org/learn/execution/#root-fields-resolvers)**
|
||||||
|
|
||||||
|
**Queries** are just **fields** on a type called "query". They can take arguments, and they
|
||||||
|
must resolve.
|
||||||
|
|
||||||
|
There's a bit more to it than that, and if you want to learn more about GraphQL, you can read
|
||||||
|
the [full documentation](https://graphql.org/learn/), but for now, these three concepts will
|
||||||
|
serve almost all of your needs to get started.
|
||||||
|
|
||||||
|
### Initial setup
|
||||||
|
|
||||||
|
To start your first schema, open a new configuration file. Let's call it `graphql.yml`.
|
||||||
|
|
||||||
|
**app/_config/graphql.yml**
|
||||||
|
```yml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
# your schemas here
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's populate a schema that is pre-configured for us out of the box, `default`.
|
||||||
|
|
||||||
|
**app/_config/graphql.yml**
|
||||||
|
```yml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
types:
|
||||||
|
# your generic types here
|
||||||
|
models:
|
||||||
|
# your dataobjects here
|
||||||
|
queries:
|
||||||
|
# your queries here
|
||||||
|
mutations:
|
||||||
|
# your mutations here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avoid config flushes
|
||||||
|
|
||||||
|
Because the schema YAML is only consumed at build time and never used at runtime, it doesn't
|
||||||
|
make much sense to store it in the configuration layer, because it just means you'll
|
||||||
|
have to `flush=1` every time you make a schema update, which will slow down your builds.
|
||||||
|
|
||||||
|
It is recommended that you store your schema YAML **outside of the _config directory** to
|
||||||
|
increase performance and remove the need for flushing.
|
||||||
|
|
||||||
|
We can do this by adding a `src` key to our schema definition that maps to a directory
|
||||||
|
relative to the project root.
|
||||||
|
|
||||||
|
**app/_config/graphql.yml**
|
||||||
|
```yml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
src: app/_graphql
|
||||||
|
```
|
||||||
|
|
||||||
|
It can also be an array of directories.
|
||||||
|
|
||||||
|
**app/_config/graphql.yml**
|
||||||
|
```yml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
src:
|
||||||
|
myDir: app/_graphql
|
||||||
|
myOtherDir: module/_graphql
|
||||||
|
```
|
||||||
|
|
||||||
|
[info]
|
||||||
|
Your directory can also be a module reference, e.g. `somevendor/somemodule: _graphql`
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
Now, in our `app/_graphql` file, we can create YAML file definitions.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
This doesn't mean there is never a need to flush your schema config. If you were to add a new
|
||||||
|
one, or make a change to the value of this `src` attribute, those are still a standard config changes.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
# no schema key needed. it's implied!
|
||||||
|
types:
|
||||||
|
# your generic types here
|
||||||
|
models:
|
||||||
|
# your dataobjects here
|
||||||
|
queries:
|
||||||
|
# your queries here
|
||||||
|
mutations:
|
||||||
|
# your mutations here
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Namespacing your schema files
|
||||||
|
|
||||||
|
Your schema YAML file will get quite bloated if it's just used as a monolithic source of truth
|
||||||
|
like this. We can tidy this up quite a bit by simply placing the files in directories that map
|
||||||
|
to the keys they populate -- e.g. `types/`, `models/`, `queries/`, `mutations/`, etc.
|
||||||
|
|
||||||
|
There are two approaches to namespacing:
|
||||||
|
* By filename
|
||||||
|
* By directory name
|
||||||
|
|
||||||
|
##### Namespacing by directory name
|
||||||
|
|
||||||
|
If you use a parent directory name (at any depth) of one of the four keywords above, it will
|
||||||
|
be implicitly placed in the corresponding section of the schema.
|
||||||
|
|
||||||
|
**app/_graphql/types/types.yml**
|
||||||
|
```yaml
|
||||||
|
# my type definitions here
|
||||||
|
```
|
||||||
|
|
||||||
|
**app/_graphql/models/models.yml**
|
||||||
|
```yaml
|
||||||
|
# my type definitions here
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Namespacing by filename
|
||||||
|
|
||||||
|
If the filename is named one of the four keywords above, it will be implicitly placed
|
||||||
|
in the corresponding section of the schema. **This only works in the root source directory**.
|
||||||
|
|
||||||
|
**app/_graphql/types.yml**
|
||||||
|
```yaml
|
||||||
|
# my types here
|
||||||
|
```
|
||||||
|
|
||||||
|
**app/_graphql/models.yml**
|
||||||
|
```yaml
|
||||||
|
# my models here
|
||||||
|
```
|
||||||
|
#### Going even more granular
|
||||||
|
|
||||||
|
These special directories can contain multiple files that will all merge together, so you can even
|
||||||
|
create one file per type, or some other convention. All that matters is that the parent directory name
|
||||||
|
matches one of the schema keys.
|
||||||
|
|
||||||
|
The following are perfectly valid:
|
||||||
|
|
||||||
|
* `app/_graphql/types/mySingleType.yml`
|
||||||
|
* `app/_graphql/models/allElementalBlocks.yml`
|
||||||
|
* `app/_graphql/news-and-blog/models/blog.yml`
|
||||||
|
* `app/_graphql/mySchema.yml`
|
||||||
|
|
||||||
|
### Changing schema defaults
|
||||||
|
|
||||||
|
In addition to all the keys mentioned above, each schema can declare a couple of generic
|
||||||
|
configuration files, `defaults` and `modelConfig`. These are
|
||||||
|
mostly used for assigning or removing default plugins to models and operations.
|
||||||
|
|
||||||
|
[info]
|
||||||
|
As of now, the only one of these being used
|
||||||
|
is `modelConfig`, but `defaults` could some day apply non-model configuration to the schema.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
Like the other sections, it can have its own `modelConfig.yml`, or just be added as a `modelConfig:`
|
||||||
|
mapping to a generic schema yaml document.
|
||||||
|
|
||||||
|
**app/_graphql/modelConfig.yml**
|
||||||
|
```yaml
|
||||||
|
DataObject:
|
||||||
|
plugins:
|
||||||
|
inheritance: true
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
readVersion: false
|
||||||
|
paginateList: false
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Defining a basic type
|
||||||
|
|
||||||
|
Let's define a generic type for our GraphQL schema.
|
||||||
|
|
||||||
|
**app/_graphql/types.yml***
|
||||||
|
```yaml
|
||||||
|
Country:
|
||||||
|
fields:
|
||||||
|
name: String
|
||||||
|
code: String
|
||||||
|
population: Int
|
||||||
|
languages: '[String]'
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're familiar with [GraphQL type language](https://graphql.org/learn/schema/#type-language), this should look pretty familiar. There are only a handful of [scalar types](https://graphql.org/learn/schema/#scalar-types) available in
|
||||||
|
GraphQL by default. They are:
|
||||||
|
|
||||||
|
* String
|
||||||
|
* Int
|
||||||
|
* Float
|
||||||
|
* Boolean
|
||||||
|
|
||||||
|
To define a type as a list, you wrap it in brackets: `[String]`, `[Int]`
|
||||||
|
|
||||||
|
To define a type as required (non-null), you add an exclamation mark: `String!`
|
||||||
|
|
||||||
|
Often times, you may want to do both: `[String!]!`
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
Look out for the footgun, here. Make sure your bracketed type is in quotes, otherwise it's valid YAML that will get parsed as an array!
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
That's all there is to it! To learn how we can take this further, check out the
|
||||||
|
[working with generic types](../working_with_generic_types) documentation. Otherwise,
|
||||||
|
let's get started on [**adding some dataobjects**](../working_with_dataobjects).
|
||||||
|
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
title: Building the schema
|
||||||
|
summary: Turn your schema configuration into executable code
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Building the schema
|
||||||
|
|
||||||
|
The primary API surface of the `silverstripe-graphql` module is the configuration YAML, and
|
||||||
|
some [procedural configuration](using_procedual_code) as well. It is important to understand
|
||||||
|
that **none of this configuration gets interpreted at runtime**. Loading the schema configuration
|
||||||
|
at runtime and converting it to executable code has dire effects on performance, making
|
||||||
|
API requests slower and slower as the schema grows larger.
|
||||||
|
|
||||||
|
To mitigate this problem, the schema that gets executed at runtime is **generated PHP code**.
|
||||||
|
This code generation happens during a build step, and it is critical to run this build step
|
||||||
|
whenever the schema changes.
|
||||||
|
|
||||||
|
### Running the build
|
||||||
|
|
||||||
|
The task that generates the schema code is `build-schema`. It takes a parameter of `schema`, whose value should be the name of the schema you want to build.
|
||||||
|
|
||||||
|
`$ vendor/bin/sake dev/graphql/build schema=default`
|
||||||
|
|
||||||
|
Keep in mind that many of your changes will be in YAML, which also requires a flush.
|
||||||
|
|
||||||
|
`$ vendor/bin/sake dev/graphql/build schema=default flush=1
|
||||||
|
|
||||||
|
[info]
|
||||||
|
If you do not provide a `schema` parameter, the task will build all schemas.
|
||||||
|
[/info]`
|
||||||
|
|
||||||
|
### Building on dev/build
|
||||||
|
|
||||||
|
By default, all schemas will be built as a side-effect of `dev/build`. To disable this, change
|
||||||
|
the config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Extensions\DevBuildExtension:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
Generating code is a pretty expensive process. A large schema with 50 dataobject classes exposing
|
||||||
|
all their operations can take up to **20 seconds** to generate. This may be acceptable
|
||||||
|
for initial builds and deployments, but during incremental development this can really
|
||||||
|
slow things down.
|
||||||
|
|
||||||
|
To mitigate this, the generated code for each type is cached against a signature.
|
||||||
|
If the type hasn't changed, it doesn't re-render. This reduces build times to **under one second** for incremental changes.
|
||||||
|
|
||||||
|
#### Clearing the cache
|
||||||
|
|
||||||
|
Normally, we'd use `flush=1` to clear the cache, but since you almost always need to run `flush=1` with the build task, it isn't a good fit. Instead, use `clear=1`.
|
||||||
|
|
||||||
|
`$ vendor/bin/sake dev/graphql/build schema=default clear=1`
|
||||||
|
|
||||||
|
If your schema is producing unexpected results, try using `clear=1` to eliminate the possibility
|
||||||
|
of a caching issue. If the issue is resolved, record exactly what you changed and [create an issue](https://github.com/silverstripe/silverstripe-graphql/issues/new).
|
||||||
|
|
||||||
|
### Build gotchas
|
||||||
|
|
||||||
|
Keep in mind that it's not always explicit schema configuration changes that require a build.
|
||||||
|
Anything influencing the output of the schema will require a build. This could include
|
||||||
|
tangential changes such as:
|
||||||
|
|
||||||
|
* Updating the `$db` array (or relationships) of a DataObject that has `fields: '*'`.
|
||||||
|
* Adding a new resolver for a type that uses [resolver discovery](../working_with_generic_types/resolver_discovery)
|
||||||
|
* Adding an extension to a DataObject
|
||||||
|
* Adding a new subclass to a DataObject that is already exposed
|
||||||
|
|
||||||
|
### Viewing the generated code
|
||||||
|
|
||||||
|
TODO, once we figure out where it will go
|
||||||
|
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: Building a schema with procedural code
|
||||||
|
summary: Use PHP code to build your schema
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Building a schema with procedural code
|
||||||
|
|
||||||
|
Sometimes you need access to dynamic information to populate your schema. For instance, you
|
||||||
|
may have an enum containing a list of all the languages that are configured for the website. It
|
||||||
|
wouldn't make sense to build this statically. It makes more sense to have a single source
|
||||||
|
of truth.
|
||||||
|
|
||||||
|
Internally, model-driven types that conform to the shapes of their models must use procedural code to add fields, create operations, and more, because the entire premise of model-driven
|
||||||
|
types is that they're dynamic. So the procedural API for schemas has to be pretty robust.
|
||||||
|
|
||||||
|
Lastly, if you just prefer writing PHP to writing YAML, this is a good option, too.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
One thing you cannot do with the procedural API, though it may be tempting, is define resolvers
|
||||||
|
on the fly as closures. Resolvers must be static methods on a class, and are evaluated during
|
||||||
|
the schema build.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
### Adding a schema builder
|
||||||
|
|
||||||
|
We can use the `builders` section of the config to add an implementation of `SchemaUpdater`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
builders:
|
||||||
|
- 'MyProject\MySchema'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now just implement the `SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater` interface.
|
||||||
|
|
||||||
|
**app/src/MySchema.php**
|
||||||
|
```php
|
||||||
|
use SilverStripe\GraphQL\Schema\Interfaces\SchemaUpdater;
|
||||||
|
use SilverStripe\GraphQL\Schema\Schema;
|
||||||
|
|
||||||
|
class MySchema implements SchemaUpdater
|
||||||
|
{
|
||||||
|
public static function updateSchema(Schema $schema): void
|
||||||
|
{
|
||||||
|
// update here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example code
|
||||||
|
|
||||||
|
Most the API should be self-documenting, and a good IDE should autocomplete everything you
|
||||||
|
need, but the key methods map directly to their configuration counterparts:
|
||||||
|
|
||||||
|
* types (`->addType(Type $type)`)
|
||||||
|
* models (`->addModel(ModelType $type)`)
|
||||||
|
* queries (`->addQuery(Query $query)`)
|
||||||
|
* mutations (`->addMutation(Mutation $mutation)`)
|
||||||
|
* enums (`->addEnum(Enum $type)`)
|
||||||
|
* interfaces (`->addInterface(InterfaceType $type)`)
|
||||||
|
* unions (`->addUnion(UnionType $type)`)
|
||||||
|
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function updateSchema(Schema $schema): void
|
||||||
|
{
|
||||||
|
$myType = Type::create('Country')
|
||||||
|
->addField('name', 'String')
|
||||||
|
->addField('code', 'String');
|
||||||
|
$schema->addType($myType);
|
||||||
|
|
||||||
|
$myQuery = Query::create('readCountries', '[Country]')
|
||||||
|
->addArg('limit', 'Int');
|
||||||
|
|
||||||
|
$myModel = ModelType::create(MyDataObject::class)
|
||||||
|
->addAllFields()
|
||||||
|
->addAllOperations();
|
||||||
|
$schema->addModel($myModel);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fluent setters
|
||||||
|
|
||||||
|
To make your code chainable, when adding fields and arguments, you can invoke a callback
|
||||||
|
to update it on the fly.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$myType = Type::create('Country')
|
||||||
|
->addField('name', 'String', function (Field $field) {
|
||||||
|
|
||||||
|
// Must be a callable. No inline closures allowed!
|
||||||
|
$field->setResolver([MyClass::class, 'myResolver'])
|
||||||
|
->addArg('myArg', 'String!');
|
||||||
|
})
|
||||||
|
->addField('code', 'String');
|
||||||
|
$schema->addType($myType);
|
||||||
|
|
||||||
|
$myQuery = Query::create('readCountries', '[Country]')
|
||||||
|
->addArg('limit', 'Int', function (Argument $arg) {
|
||||||
|
$arg->setDefaultValue(20);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Getting started
|
||||||
|
summary: Open up your first GraphQL server and build your schema
|
||||||
|
icon: rocket
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
This section of the documentation will give you an overview of how to get a simple GraphQL API
|
||||||
|
up and running with some dataobject content.
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,288 @@
|
|||||||
|
---
|
||||||
|
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 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 definition in the `modelConfig`
|
||||||
|
section.
|
||||||
|
|
||||||
|
### 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/modelConfig.yml*
|
||||||
|
```yaml
|
||||||
|
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/modelConfig.yml*
|
||||||
|
```yaml
|
||||||
|
DataObject
|
||||||
|
type_prefix: 'MyProject'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,324 @@
|
|||||||
|
---
|
||||||
|
title: DataObject query plugins
|
||||||
|
summary: Learn about some of the useful goodies that come pre-packaged with DataObject queries
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
## DataObject query plugins
|
||||||
|
|
||||||
|
This module has a [plugin system](../plugins) 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
|
||||||
|
* 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` and `offset` 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:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readPages(limit: 10, offset: 20) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
totalCount
|
||||||
|
hasNextPage
|
||||||
|
hasPrevPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
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](https://graphql.org/learn/pagination/), which
|
||||||
|
isn't something we do in Silverstripe CMS.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
#### Disabling pagination
|
||||||
|
|
||||||
|
Just set it to `false` in the configuration.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\ProductCategory:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
paginateList: false
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable pagination globally, use `modelConfig`:
|
||||||
|
|
||||||
|
*app/_graphql/modelConfig.yml*
|
||||||
|
```yaml
|
||||||
|
DataObject:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
paginateList: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### The filter plugin
|
||||||
|
|
||||||
|
The filter plugin (`SilverStripe\GraphQL\Schema\DataObject\Plugin\QueryFilter`) adds a
|
||||||
|
special `filter` argument to the `read` and `readOne` operations.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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:
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readPages (
|
||||||
|
filter: {
|
||||||
|
title: { ne: "Home" },
|
||||||
|
created: { gt: "2020-06-01", lte: "2020-09-01" }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
While it is possible to filter using multiple comparators, segmenting them into
|
||||||
|
disjunctive groups (e.g. "OR" and "AND" clauses) is not yet supported.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
Nested fields are supported by default:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
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*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\ProductCategory:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
featured: true
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
filter:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Disabling the filter plugin
|
||||||
|
|
||||||
|
Just set it to `false` in the configuration.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\ProductCategory:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
filter: false
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable filtering globally, use `modelConfig`:
|
||||||
|
|
||||||
|
*app/_graphql/modelConfig.yml*
|
||||||
|
```yaml
|
||||||
|
DataObject:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
filter: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### The sort plugin
|
||||||
|
|
||||||
|
The sort plugin (`SilverStripe\GraphQL\Schema\DataObject\Plugin\QuerySort`) adds a
|
||||||
|
special `sort` argument to the `read` and `readOne` operations.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readPages (
|
||||||
|
sort: { created: DESC }
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested fields are supported by default, but only for linear relationships (e.g `has_one`):
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readProducts(
|
||||||
|
sort: {
|
||||||
|
primaryCategory: {
|
||||||
|
lastEdited: DESC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### 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*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\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*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\ProductCategory:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
sort: false
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable sort globally, use `modelConfig`:
|
||||||
|
|
||||||
|
*app/_graphql/modelConfig.yml*
|
||||||
|
```yaml
|
||||||
|
DataObject:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
sort: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 filter argument
|
||||||
|
called `link` (configurable on the `field_name` property of `LinkablePlugin`).
|
||||||
|
|
||||||
|
When the `filter` plugin is also activated for the query (it is by default for readOne), the `link` field will be added to the filter
|
||||||
|
input type. Note that all other filters won't apply in this case, as `link`, like `id`, is exclusive
|
||||||
|
by definition.
|
||||||
|
|
||||||
|
If the `filter` plugin is not activated for the query, a new `link` argument will be added to the query
|
||||||
|
on its own.
|
||||||
|
|
||||||
|
With the standard `filter` plugin applied:
|
||||||
|
```graphql
|
||||||
|
readOneSiteTree(filter: { link: "/about-us" }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the `filter` plugin is disabled:
|
||||||
|
```graphql
|
||||||
|
readOneSiteTree(link: "/about-us" ) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: DataObject operation permissions
|
||||||
|
summary: A look at how permissions work for DataObject 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]
|
||||||
|
|
||||||
|
## DataObject operation permissions
|
||||||
|
|
||||||
|
Any of the operations that come pre-configured for DataObjects are secured by the appropriate permissions
|
||||||
|
by default.
|
||||||
|
Please see [Model-Level Permissions](/model/permissions/#model-level-permissions) for more information.
|
||||||
|
|
||||||
|
### Mutation permssions
|
||||||
|
|
||||||
|
[info]
|
||||||
|
When mutations fail due to permission checks, they throw a `PermissionsException`.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
For `create`, if a singleton instance of the record being created doesn't pass a `canCreate($member)` check,
|
||||||
|
the mutation will throw.
|
||||||
|
|
||||||
|
For `update`, if the record matching the given ID doesn't pass a `canEdit($member)` check, the mutation will
|
||||||
|
throw.
|
||||||
|
|
||||||
|
For `delete`, if any of the given IDs don't pass a `canDelete($member)` check, the mutation will throw.
|
||||||
|
|
||||||
|
### Query permissions
|
||||||
|
|
||||||
|
Query permissions are a bit more complicated, because they can either be in list form, (paginated or not),
|
||||||
|
or a single item. Rather than throw, these permission checks work as filters.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
It is critical that you have a `canView()` method defined on your dataobjects. Without this, only admins are
|
||||||
|
assumed to have permission to view a record.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
|
||||||
|
For `read` and `readOne` a plugin called `canView` will filter the result set by the `canView($memeber)` check.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
When paginated items fail a `canView()` check, the `pageInfo` field is not affected.
|
||||||
|
Limits and pages are determined through database queries,
|
||||||
|
it would be too inefficient to perform in-memory checks on large data sets.
|
||||||
|
This can result in pages
|
||||||
|
showing a smaller number of items than what the page should contain, but keeps the pagination calls consistent
|
||||||
|
for `limit` and `offset` parameters.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
### Disabling query permissions
|
||||||
|
|
||||||
|
Though not recommended, you can disable query permissions by setting their plugins to `false`.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
Page:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
canView: false
|
||||||
|
readOne:
|
||||||
|
plugins:
|
||||||
|
canView: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
title: DataObject inheritance
|
||||||
|
summary: Learn how inheritance is handled in DataObject types
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
## DataObject inheritance
|
||||||
|
|
||||||
|
The inheritance pattern used in the ORM is a tricky thing to navigate in a GraphQL API, mostly owing
|
||||||
|
to the fact that there is no concept of inheritance in GraphQL types. The main tools we have at our
|
||||||
|
disposal are [interfaces](https://graphql.org/learn/schema/#interfaces) and [unions](https://graphql.org/learn/schema/#union-types) to deal with this type of architecture, but in practise, it quickly becomes cumbersome.
|
||||||
|
For instance, just adding a subclass to a DataObject can force the return type to change from a simple list
|
||||||
|
of types to a union of multiple types, and this would break frontend code.
|
||||||
|
|
||||||
|
While more conventional, unions and interfaces introduce more complexity, and given how much we rely
|
||||||
|
on inheritance in Silverstripe CMS, particularly with `SiteTree`, inheritance in GraphQL is handled in a less
|
||||||
|
conventional but more ergonomic way using a plugin called `inheritance`.
|
||||||
|
|
||||||
|
### Introducing pseudo-unions
|
||||||
|
|
||||||
|
Let's take a simple example. Imagine we have this design:
|
||||||
|
|
||||||
|
```
|
||||||
|
> SiteTree (fields: title, content)
|
||||||
|
> Page (fields: pageField)
|
||||||
|
> NewsPage (fields: newsPageField)
|
||||||
|
> Contact Page (fields: contactPageField)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's expose `Page` to graphql:
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
Page:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
content: true
|
||||||
|
pageField: true
|
||||||
|
operations: '*'
|
||||||
|
NewsPage:
|
||||||
|
fields:
|
||||||
|
newsPageField: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's how we can query the inherited fields:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query readPages {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
content
|
||||||
|
pageField
|
||||||
|
_extend {
|
||||||
|
NewsPage {
|
||||||
|
newsPageField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `_extend` field is semantically aligned with is PHP counterpart -- it's an object whose fields are the
|
||||||
|
names of all the types that are descendants of the parent type. Each of those objects contains all the fields
|
||||||
|
on that type, both inherited and native.
|
||||||
|
|
||||||
|
[info]
|
||||||
|
The `_extend` field is only available on base classes, e.g. `Page` in the example above.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
### Implicit exposure
|
||||||
|
|
||||||
|
By exposing `Page`, we implicitly expose *all of its ancestors* and *all of its descendants*. Adding `Page`
|
||||||
|
to our schema implies that we also want its parent `SiteTree` in the schema (after all, that's where most of its fields
|
||||||
|
come from), but we also need to be mindful that queries for page will return descendants of `Page`, as well.
|
||||||
|
|
||||||
|
But these types are implicitly added to the schema, what are their fields?
|
||||||
|
|
||||||
|
The answer is *only the fields you've already opted into*. Parent classes will apply the fields exposed
|
||||||
|
by their descendants, and descendant classes will only expose their ancestors' exposed fields.
|
||||||
|
If you are opting into all fields on a model (`fields: "*"`), this only applies to the
|
||||||
|
model itself, not its subclasses.
|
||||||
|
|
||||||
|
In our case, we've exposed:
|
||||||
|
|
||||||
|
* `title` (on `SiteTree`)
|
||||||
|
* `content` (on `SiteTree`)
|
||||||
|
* `pageField` (on `Page`)
|
||||||
|
* `newsPageField` (on `NewsPage`)
|
||||||
|
|
||||||
|
The `Page` type will contain the following fields:
|
||||||
|
|
||||||
|
* `id` (required for all DataObject types)
|
||||||
|
* `title`
|
||||||
|
* `content`
|
||||||
|
* `pageField`
|
||||||
|
|
||||||
|
And the `NewsPage` type would contain the following fields:
|
||||||
|
|
||||||
|
* `newsPageField`
|
||||||
|
|
||||||
|
[info]
|
||||||
|
Operations are not implicitly exposed. If you add a `read` operation to `SiteTree`, you will not get one for
|
||||||
|
`NewsPage` and `ContactPage`, etc. You have to opt in to those.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
### Pseudo-unions fields are de-duped
|
||||||
|
|
||||||
|
To keep things tidy, the pseudo unions in the `_extend` field remove any fields that are already in
|
||||||
|
the parent.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query readPages {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
content
|
||||||
|
_extend {
|
||||||
|
NewsPage {
|
||||||
|
title <---- Doesn't exist
|
||||||
|
newsPageField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
title: Versioned content
|
||||||
|
summary: A guide on how DataObjects with the Versioned extension behave in GraphQL schemas
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
## Versioned content
|
||||||
|
|
||||||
|
For the most part, if your DataObject has the `Versioned` extension applied, there is nothing you need to do
|
||||||
|
explicitly, but be aware that it will affect the operations and fields of your type.
|
||||||
|
|
||||||
|
### Versioned plugins
|
||||||
|
|
||||||
|
There are several plugins provided by the `silverstripe-versioned` module that affect how versioned DataObjects
|
||||||
|
appear in the schema. These include:
|
||||||
|
|
||||||
|
* The `versioning` plugin, applied to the DataObject type
|
||||||
|
* The `readVersion` plugin, applied to the queries for the DataObject
|
||||||
|
* The `unpublishOnDelete` plugin, applied to the delete mutation
|
||||||
|
|
||||||
|
Let's walk through each one.
|
||||||
|
|
||||||
|
#### The `versioning` plugin
|
||||||
|
|
||||||
|
Defined in the `SilverStripe\Versioned\GraphQL\Plugins\VersionedDataObject` class, this plugin adds
|
||||||
|
several fields to the DataObject type, including:
|
||||||
|
|
||||||
|
##### The `version` field
|
||||||
|
|
||||||
|
The `version` field on your DataObject will include the following fields:
|
||||||
|
|
||||||
|
* `author`: Member (Object -- the author of the version)
|
||||||
|
* `publisher`: Member (Object -- the publisher of the version)
|
||||||
|
* `published`: Boolean (True if the version is published)
|
||||||
|
* `liveVersion`: Boolean (True if the version is the one that is currently live)
|
||||||
|
* `latestDraftVersion`: Boolean (True if the version is the latest draft version)
|
||||||
|
|
||||||
|
Let's look at it in context:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query readPages {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
version {
|
||||||
|
author {
|
||||||
|
firstname
|
||||||
|
}
|
||||||
|
published
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### The `versions` field
|
||||||
|
|
||||||
|
The `versions` field on your DataObject will return a list of the `version` objects described above.
|
||||||
|
The list is sortable by version number, using the `sort` parameter.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query readPages {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
versions(sort: { version: DESC }) {
|
||||||
|
author {
|
||||||
|
firstname
|
||||||
|
}
|
||||||
|
published
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### The `readVersion` plugin
|
||||||
|
|
||||||
|
This plugin updates the `read` operation to include a `versioning` argument that contains the following
|
||||||
|
fields:
|
||||||
|
|
||||||
|
* `mode`: VersionedQueryMode (An enum of [`ARCHIVE`, `LATEST`, `DRAFT`, `LIVE`, `STATUS`, `VERSION`])
|
||||||
|
* `archiveDate`: String (The archive date to read from)
|
||||||
|
* `status`: VersionedStatus (An enum of [`PUBLISHED`, `DRAFT`, `ARCHIVED`, `MODIFIED`])
|
||||||
|
* `version`: Int (The exact version to read)
|
||||||
|
|
||||||
|
The query will automatically apply the settings from the `versioning` input type to the query and affect
|
||||||
|
the resulting `DataList`.
|
||||||
|
|
||||||
|
|
||||||
|
#### The "unpublishOnDelete" plugin
|
||||||
|
|
||||||
|
This is mostly for internal use. It's an escape hatch for tidying up after a delete.
|
||||||
|
|
||||||
|
### Versioned operations
|
||||||
|
|
||||||
|
DataObjects with the `Versioned` extension applied will also receive four extra operations
|
||||||
|
by default. They include:
|
||||||
|
|
||||||
|
* `publish`
|
||||||
|
* `unpublish`
|
||||||
|
* `copyToStage`
|
||||||
|
* `rollback`
|
||||||
|
|
||||||
|
All of these identifiers can be used in the `operations` config for your versioned
|
||||||
|
DataObject. They will all be included if you use `operations: '*'`.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\MyObject:
|
||||||
|
fields: '*'
|
||||||
|
operations:
|
||||||
|
publish: true
|
||||||
|
unpublish: true
|
||||||
|
rollback: true
|
||||||
|
copyToStage: true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using the operations
|
||||||
|
|
||||||
|
Let's look at a few examples:
|
||||||
|
|
||||||
|
**Publishing**
|
||||||
|
```graphql
|
||||||
|
mutation publishSiteTree(id: 123) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unpublishing**
|
||||||
|
```graphql
|
||||||
|
mutation unpublishSiteTree(id: 123) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rolling back**
|
||||||
|
```graphql
|
||||||
|
mutation rollbackSiteTree(id: 123, toVersion: 5) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Copying to stage**
|
||||||
|
```graphql
|
||||||
|
mutation copySiteTreeToStage(id: 123, fromStage: DRAFT, toStage: LIVE) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
title: Property mapping and dot syntax
|
||||||
|
summary: Learn how to customise field names, use dot syntax, and use aggregate functions
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
## Property mapping and dot syntax
|
||||||
|
|
||||||
|
For the most part, field names are inferred through the DataObject model, but its API affords developers full
|
||||||
|
control over naming:
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
Page:
|
||||||
|
fields:
|
||||||
|
pageContent:
|
||||||
|
type: String
|
||||||
|
property: Content
|
||||||
|
```
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
When using explicit property mapping, you must also define an explicit type, as it can
|
||||||
|
no longer be inferred.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
|
||||||
|
### Dot-separated accessors
|
||||||
|
|
||||||
|
Property mapping is particularly useful when using **dot syntax** to access fields.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Pages\Blog:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
authorName:
|
||||||
|
type: String
|
||||||
|
property: 'Author.FirstName'
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields on plural relationships will automatically convert to a `column` array:
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Pages\Blog:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
categoryTitles:
|
||||||
|
type: '[String]'
|
||||||
|
property: 'Categories.Title'
|
||||||
|
authorsFavourites:
|
||||||
|
type: '[String]'
|
||||||
|
property: 'Author.FavouritePosts.Title'
|
||||||
|
```
|
||||||
|
|
||||||
|
We can even use a small subset of **aggregates**, including `Count()`, `Max()`, `Min()` and `Avg()`.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\ProductCategory:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
productCount:
|
||||||
|
type: Int
|
||||||
|
property: 'Products.Count()'
|
||||||
|
averageProductPrice:
|
||||||
|
type: Float
|
||||||
|
property: 'Products.Avg(Price)'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Nested type definitions
|
||||||
|
summary: Define dependent types inline with a parent type
|
||||||
|
---
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
## Nested type definitions
|
||||||
|
|
||||||
|
For readability and ergonomics, you can take advantage of nested type definitions. Let's imagine
|
||||||
|
we have a `Blog` and we want to expose `Author` and `Categories`, but while we're at it, we want
|
||||||
|
to specify what fields they should have.
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Pages\Blog:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
author:
|
||||||
|
fields:
|
||||||
|
firstName: true
|
||||||
|
surname: true
|
||||||
|
email: true
|
||||||
|
categories:
|
||||||
|
fields: '*'
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, we could flatten that out:
|
||||||
|
|
||||||
|
*app/_graphql/models.yml*
|
||||||
|
```yaml
|
||||||
|
MyProject\Pages\Blog:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
author: true
|
||||||
|
categories: true
|
||||||
|
SilverStripe\Securty\Member:
|
||||||
|
fields
|
||||||
|
firstName: true
|
||||||
|
surname: true
|
||||||
|
email: true
|
||||||
|
MyProject\Models\BlogCategory:
|
||||||
|
fields: '*'
|
||||||
|
```
|
||||||
|
|
||||||
|
[info]
|
||||||
|
You cannot define operations on nested types. They only accept fields.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Working with DataObjects
|
||||||
|
summary: Add DataObjects to your schema, expose their fields, read/write operations, and more
|
||||||
|
icon: database
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with DataObjects
|
||||||
|
|
||||||
|
In this section of the documentation, we'll cover adding DataObjects to the schema, exposing their fields,
|
||||||
|
and adding read/write operations. Additionally, we'll cover some of the plugins that are available to DataObjects
|
||||||
|
like [sorting, filtering, and pagination](query_plugins), as well as some more advanced concepts like
|
||||||
|
[permissions](permissions), [inheritance](inheritance) and [property mapping](property_mapping).
|
||||||
|
|
||||||
|
[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]
|
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: Creating a generic type
|
||||||
|
summary: Creating a type that doesn't map to a DataObject
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with Generic Types
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Creating a generic type
|
||||||
|
|
||||||
|
Let's create a simple type that will work with the inbuilt features of Silverstripe CMS.
|
||||||
|
We'll define some languages based on the `i18n` API.
|
||||||
|
|
||||||
|
*app/_graphql/types.yml*
|
||||||
|
```yaml
|
||||||
|
Country:
|
||||||
|
fields:
|
||||||
|
code: String!
|
||||||
|
name: String!
|
||||||
|
```
|
||||||
|
|
||||||
|
We've defined a type called `Country` that has two fields: `code` and `name`. An example record
|
||||||
|
could be something like:
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
'code' => 'bt',
|
||||||
|
'name' => 'Bhutan'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
That's all we have to do for now! Let's move on to [building a custom query](building_a_custom_query) to see how we
|
||||||
|
can use it.
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
title: Building a custom query
|
||||||
|
summary: Add a custom query for any type of data
|
||||||
|
---
|
||||||
|
# Working with generic types
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Building a custom query
|
||||||
|
|
||||||
|
We've now defined the shape of our data, now we need to build a way to access it. For this,
|
||||||
|
we'll need a query. Let's add one to the `queries` section of our config.
|
||||||
|
|
||||||
|
*app/_graphql/schema.yml*
|
||||||
|
```yaml
|
||||||
|
types:
|
||||||
|
Country:
|
||||||
|
fields:
|
||||||
|
code: String!
|
||||||
|
name: String!
|
||||||
|
queries:
|
||||||
|
readCountries: '[Country]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we have a query that will return all the countries. In order to make this work, we'll
|
||||||
|
need a **resolver**. For this, we're going to have to break out of the configuration layer
|
||||||
|
and write some code.
|
||||||
|
|
||||||
|
**app/src/Resolvers/MyResolver.php**
|
||||||
|
```php
|
||||||
|
class MyResolver
|
||||||
|
{
|
||||||
|
public static function resolveCountries(): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
$countries = Injector::inst()->get(Locales::class)->getCountries();
|
||||||
|
foreach ($countries as $code => $name) {
|
||||||
|
$results[] = [
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolvers are pretty loosely defined, and don't have to adhere to any specific contract
|
||||||
|
other than that they **must be static methods**. You'll see why when we add it to the configuration:
|
||||||
|
|
||||||
|
*app/_graphql/schema.yml
|
||||||
|
```yaml
|
||||||
|
types:
|
||||||
|
Country:
|
||||||
|
fields:
|
||||||
|
code: String!
|
||||||
|
name: String!
|
||||||
|
queries:
|
||||||
|
readCountries:
|
||||||
|
type: '[Country]'
|
||||||
|
resolver: [ 'MyResolver', 'resolveCountries' ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, we just have to build the schema:
|
||||||
|
|
||||||
|
`$ vendor/bin/sake dev/graphql/build schema=default`
|
||||||
|
|
||||||
|
Let's test this out in our GraphQL IDE. If you have the [graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools) module installed, just open it up and set it to the `/graphql` endpoint.
|
||||||
|
|
||||||
|
As you start typing, it should autocomplete for you.
|
||||||
|
|
||||||
|
Here's our query:
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readCountries {
|
||||||
|
name
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And the expected response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"readCountries": [
|
||||||
|
{
|
||||||
|
"name": "Afghanistan",
|
||||||
|
"code": "af"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Åland Islands",
|
||||||
|
"code": "ax"
|
||||||
|
},
|
||||||
|
"... etc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
Keep in mind that [plugins](../02_working_with_dataobjects/02_query_plugins.md)
|
||||||
|
don't apply in this context. Most importantly, this means you need to
|
||||||
|
implement your own `canView()` checks.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
This is great, but as we write more and more queries for types with more and more fields,
|
||||||
|
it's going to get awfully laborious mapping all these resolvers. Let's clean this up a bit by
|
||||||
|
adding a bit of convention over configuration, and save ourselves a lot of time to boot. We can do
|
||||||
|
that using the [resolver discovery pattern](resolver_discovery).
|
||||||
|
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,146 @@
|
|||||||
|
---
|
||||||
|
title: The resolver discovery pattern
|
||||||
|
summary: How you can opt out of mapping fields to resolvers by adhering to naming conventions
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with generic types
|
||||||
|
|
||||||
|
[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 resolver discovery pattern
|
||||||
|
|
||||||
|
When you define a query mutation, or any other field on a type, you can opt out of providing
|
||||||
|
an explicit resolver and allow the system to discover one for you based on naming convention.
|
||||||
|
|
||||||
|
Let's start by registering a resolver class(es) where we can define a bunch of these functions.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Schema\Registry\ResolverRegistry:
|
||||||
|
constructor:
|
||||||
|
myResolver: '%$MyProject\Resolvers\MyResolvers'
|
||||||
|
```
|
||||||
|
|
||||||
|
What we're registering here is called a `ResolverProvider`, and it must implement that interface.
|
||||||
|
The only thing this class is obliged to do is return a method name for a resolver given a type name and
|
||||||
|
`Field` object. If the class does not contain a resolver for that combination, it may return null and
|
||||||
|
defer to other resolver providers, or ultimately fallback on the global default resolver.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function getResolverMethod(?string $typeName = null, ?Field $field = null): ?string;
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's look at our query again:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readCountries {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An example implementation of this method for our example might be:
|
||||||
|
|
||||||
|
* Does `resolveCountryName` exist?
|
||||||
|
* Yes? Invoke
|
||||||
|
* No? Continue
|
||||||
|
* Does `resolveCountry` exist?
|
||||||
|
* Yes? Invoke
|
||||||
|
* No? Continue
|
||||||
|
* Does `resolveName` exist?
|
||||||
|
* Yes? Invoke
|
||||||
|
* No? Continue
|
||||||
|
* Return null. Maybe someone else knows how to deal with this.
|
||||||
|
|
||||||
|
You can implement whatever logic you like to help the resolver provider discover which of its methods
|
||||||
|
it appropriate for the given type/field combination, but since the above pattern seems like a pretty common
|
||||||
|
implementation, the module ships an abstract `DefaultResolverProvider` that applies this logic. You can just
|
||||||
|
write the resolver methods!
|
||||||
|
|
||||||
|
Let's add a resolver method to our resolver provider:
|
||||||
|
|
||||||
|
**app/src/Resolvers/MyResolvers.php**
|
||||||
|
```php
|
||||||
|
use SilverStripe\GraphQL\Schema\Resolver\DefaultResolverProvider;
|
||||||
|
|
||||||
|
class MyResolvers extends DefaultResolverProvider
|
||||||
|
{
|
||||||
|
public static function resolveReadCountries()
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
$countries = Injector::inst()->get(Locales::class)->getCountries();
|
||||||
|
foreach ($countries as $code => $name) {
|
||||||
|
$results[] = [
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To recap, the `DefaultResolverProvider` will follow this workflow to locate a resolver
|
||||||
|
for this query:
|
||||||
|
|
||||||
|
* `resolveQueryReadCountries()` (<typeName><fieldName>)
|
||||||
|
* `resolveQuery()` (<typeName>)
|
||||||
|
* `resolveReadCountries()` (<fieldName>)
|
||||||
|
* `resolve` (catch-all)
|
||||||
|
|
||||||
|
|
||||||
|
Now that we're using logic to discover our resolver, we can clean up the config a bit.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yml
|
||||||
|
queries:
|
||||||
|
readCountries: '[Country]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run the schema build, with a flush, and let's go!
|
||||||
|
|
||||||
|
`$ vendor/bin/sake dev/graphql/build schema=default flush=1`
|
||||||
|
|
||||||
|
|
||||||
|
### Field resolvers
|
||||||
|
|
||||||
|
A less magical approach to resolver discovery is defining a `fieldResolver` property on your
|
||||||
|
types. This is a generic handler for all fields on a given type and can be a nice middle
|
||||||
|
ground between the rigor of hard coding everything and the opacity of discovery logic.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yml
|
||||||
|
types:
|
||||||
|
Country:
|
||||||
|
fields:
|
||||||
|
name: String
|
||||||
|
code: String
|
||||||
|
fieldResolver: [ 'MyProject\MyResolver', 'resolveCountryField' ]
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll need to do explicit checks for the `fieldName` in your resolver to make this work.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function resolveCountryField($obj, $args, $context, ResolveInfo $info)
|
||||||
|
{
|
||||||
|
$fieldName = $info->fieldName;
|
||||||
|
if ($fieldName === 'image') {
|
||||||
|
return $obj->getImage()->getURL();
|
||||||
|
}
|
||||||
|
// .. etc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: Adding arguments
|
||||||
|
summary: Add arguments to your fields, queries, and mutations
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with generic types
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Adding arguments
|
||||||
|
|
||||||
|
Fields can have arguments, and queries are just fields, so let's add a simple
|
||||||
|
way of influencing our query response:
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
queries:
|
||||||
|
'readCountries(limit: Int!)': '[Country]'
|
||||||
|
```
|
||||||
|
|
||||||
|
We've provided the required argument `limit` to the query, which will allow us to truncate the results.
|
||||||
|
Let's update the resolver accordingly.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function resolveReadCountries($obj, array $args = [])
|
||||||
|
{
|
||||||
|
$limit = $args['limit'];
|
||||||
|
$results = [];
|
||||||
|
$countries = Injector::inst()->get(Locales::class)->getCountries();
|
||||||
|
$countries = array_slice($countries, 0, $limit);
|
||||||
|
foreach ($countries as $code => $name) {
|
||||||
|
$results[] = [
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's try our query again. This time, notice that the IDE is telling us we're missing a required argument.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readCountries(limit: 5) {
|
||||||
|
name
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This works pretty well, but maybe it's a bit over the top to *require* the `limit` argument. We want to optimise
|
||||||
|
performance, but we also don't want to burden the developer with tedium like this. Let's give it a default value.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
queries:
|
||||||
|
'readCountries(limit: Int = 20)': '[Country]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Rebuild the schema, and notice that the IDE is no longer yelling at you for a `limit` argument.
|
||||||
|
|
||||||
|
Let's take this a step further by turning this in to a proper [paginated result](adding_pagination).
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
title: Adding pagination
|
||||||
|
summary: Add the pagination plugin to a generic query
|
||||||
|
---
|
||||||
|
# Working with generic types
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Adding pagination
|
||||||
|
|
||||||
|
We've created a simple generic query for our `Country` type called `readCounties` that takes a `limit` argument.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readCountries(limit: 5) {
|
||||||
|
name
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's take this a step further and paginate it using a plugin.
|
||||||
|
|
||||||
|
### The "paginate" plugin
|
||||||
|
|
||||||
|
Since pagination is a fairly common task, we can take advantage of some reusable code here and just add a generic
|
||||||
|
plugin for paginating.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
If you're paginating a DataList, you might want to consider using models with read operations, which paginate
|
||||||
|
by default using the `paginateList` plugin. This will work, too, but requires a bit of code.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
Let's add the plugin to our query:
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
queries:
|
||||||
|
readCountries:
|
||||||
|
type: '[Country]'
|
||||||
|
plugins:
|
||||||
|
paginate: {}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Right now the plugin will add the necessary arguments to the query, build and update the return types. But
|
||||||
|
we still need to provide this generic plugin a way of actually limiting the result set, so we need a resolver.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
queries:
|
||||||
|
readCountries:
|
||||||
|
type: '[Country]'
|
||||||
|
plugins:
|
||||||
|
paginate:
|
||||||
|
resolver: ['MyProject\Resolvers\Resolver', 'paginateCountries']
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's write that resolver code now:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function paginateCountries(array $context): Closure
|
||||||
|
{
|
||||||
|
$maxLimit = $context['maxLimit'];
|
||||||
|
return function (array $countries, array $args) use ($maxLimit) {
|
||||||
|
$offset = $args['offset'];
|
||||||
|
$limit = $args['limit'];
|
||||||
|
$total = count($countries);
|
||||||
|
if ($limit > $maxLimit) {
|
||||||
|
$limit = $maxLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$limitedList = array_slice($countries, $offset, $limit);
|
||||||
|
|
||||||
|
return PaginationPlugin::createPaginationResult($total, $limitedList, $limit, $offset);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A couple of things are going on here:
|
||||||
|
|
||||||
|
* Notice the new design pattern of a **context-aware resolver**. Since the plugin be configured with a `maxLimit`
|
||||||
|
parameter, we need to get this information to the function that is ultimately used in the schema. Therefore,
|
||||||
|
we create a dynamic function in a static method by wrapping it with context. It's kind of like a decorator.
|
||||||
|
|
||||||
|
* As long as we can do the work of counting and limiting the array, the `PaginationPlugin` can handle the rest. It will reutrn an array including `edges`, `nodes`, and `pageInfo`.
|
||||||
|
|
||||||
|
Rebuild the schema, and test it out:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readCountries(limit:3, offset:4) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: Adding descriptions
|
||||||
|
summary: Add descriptions to just about anything in your schema to improve your developer experience
|
||||||
|
---
|
||||||
|
# Working with generic types
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Adding descriptions
|
||||||
|
|
||||||
|
One of the great features of a schema-backed API is that it is self-documenting. Many
|
||||||
|
API developers choose to maximise the benefit of this by adding descriptions to some or
|
||||||
|
all of the components of their schema.
|
||||||
|
|
||||||
|
The trade-off for using descriptions is that the YAML configuration becomes a bit more verbose.
|
||||||
|
|
||||||
|
Let's add some descriptions to our types and fields.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
types:
|
||||||
|
Country:
|
||||||
|
description: A record that describes one of the world's sovereign nations
|
||||||
|
fields:
|
||||||
|
code:
|
||||||
|
type: String!
|
||||||
|
description: The unique two-letter country code
|
||||||
|
name:
|
||||||
|
type: String!
|
||||||
|
description: The canonical name of the country, in English
|
||||||
|
```
|
||||||
|
|
||||||
|
We can also add descriptions to our query arguments. We'll have to remove the inline argument
|
||||||
|
definition to do that.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
queries:
|
||||||
|
readCountries:
|
||||||
|
type: '[Country]'
|
||||||
|
description: Get all the countries in the world
|
||||||
|
args:
|
||||||
|
limit:
|
||||||
|
type: Int = 20
|
||||||
|
description: The limit that is applied to the result set
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: Enums, unions, and interfaces
|
||||||
|
summary: Add some non-object types to your schema
|
||||||
|
---
|
||||||
|
# Working with generic types
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Enums, unions, and interfaces
|
||||||
|
|
||||||
|
In more complex schemas, you may want to define types that aren't simply a list of fields, or
|
||||||
|
"object types." These include enums, unions and interfaces.
|
||||||
|
|
||||||
|
|
||||||
|
### Enum types
|
||||||
|
|
||||||
|
Enum types are simply a list of string values that are possible for a given field. They are
|
||||||
|
often used in arguments to queries, such as `{sort: DESC }`.
|
||||||
|
|
||||||
|
It's very easy to add enum types to your schema. Just use the `enums` section of the config.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
enums:
|
||||||
|
SortDirection:
|
||||||
|
DESC: Descending order
|
||||||
|
ASC: Ascending order
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Interfaces
|
||||||
|
|
||||||
|
An interface is a specification of fields that must be included on a type that implements it.
|
||||||
|
For example, an interface `Person` could include `firstName: String`, `surname: String`, and
|
||||||
|
`age: Int`. The types `Actor` and `Chef` would implement the `Person` interface. Actors and
|
||||||
|
chefs must have names and ages.
|
||||||
|
|
||||||
|
To define an interface, use the `interfaces` section of the config.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
interfaces:
|
||||||
|
Person:
|
||||||
|
fields:
|
||||||
|
firstName: String!
|
||||||
|
surname: String!
|
||||||
|
age: Int!
|
||||||
|
resolveType: [ 'MyProject\MyResolver', 'resolvePersonType' ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Interfaces must define a `resolveType` resolver method to inform the interface
|
||||||
|
which type it is applied to given a specific result. This method is non-discoverable and
|
||||||
|
must be applied explicitly.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function resolvePersonType($object): string
|
||||||
|
{
|
||||||
|
if ($object instanceof Actor) {
|
||||||
|
return 'Actor';
|
||||||
|
}
|
||||||
|
if ($object instanceof Chef) {
|
||||||
|
return 'Chef';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Union types
|
||||||
|
|
||||||
|
A union type is used when a field can resolve to multiple types. For example, a query
|
||||||
|
for "Articles" could return a list containing both "Blog" and "NewsStory" types.
|
||||||
|
|
||||||
|
To add a union type, use the `unions` section of the configuration.
|
||||||
|
|
||||||
|
**app/_graphql/schema.yml**
|
||||||
|
```yaml
|
||||||
|
unions:
|
||||||
|
Article:
|
||||||
|
types: [ 'Blog', 'NewsStory' ]
|
||||||
|
typeResolver: [ 'MyProject\MyResolver', 'resolveArticleUnion' ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Like interfaces, unions need to know how to resolve their types. These methods are also
|
||||||
|
non-discoverable and must be applied explicitly.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function resolveArticleUnion(Article $object): string
|
||||||
|
{
|
||||||
|
if ($object->category === 'blogs')
|
||||||
|
return 'Blog';
|
||||||
|
}
|
||||||
|
if ($object->category === 'news') {
|
||||||
|
return 'NewsStory';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: Working with generic types
|
||||||
|
summary: Break away from the magic of DataObject models and build types and queries from scratch.
|
||||||
|
---
|
||||||
|
|
||||||
|
In this section of the documentation, we cover the fundamentals that are behind a lot of the magic that goes
|
||||||
|
into making DataObject types work. We'll create some types that are not based on DataObjects at all, and we'll
|
||||||
|
write some custom queries from the ground up.
|
||||||
|
|
||||||
|
[info]
|
||||||
|
Just because we won't be using DataObjects in this example doesn't mean you can't do it. You will lose a lot
|
||||||
|
of the benefits of the DataObject model, but this lower level API may suit your needs for really specific use
|
||||||
|
cases.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
|
||||||
|
[CHILDREN]
|
||||||
|
|
||||||
|
[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]
|
@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
title: Authentication
|
||||||
|
summary: Ensure your GraphQL api is only accessible to provisioned users
|
||||||
|
icon: user-lock
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security & Best Practices
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Some SilverStripe resources have permission requirements to perform CRUD operations
|
||||||
|
on, for example the `Member` object in the previous examples.
|
||||||
|
|
||||||
|
If you are logged into the CMS and performing a request from the same session then
|
||||||
|
the same Member session is used to authenticate GraphQL requests, however if you
|
||||||
|
are performing requests from an anonymous/external application you may need to
|
||||||
|
authenticate before you can complete a request.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
Please note that when implementing GraphQL resources it is the developer's
|
||||||
|
responsibility to ensure that permission checks are implemented wherever
|
||||||
|
resources are accessed.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
### Default authentication
|
||||||
|
|
||||||
|
The `MemberAuthenticator` class is configured as the default option for authentication,
|
||||||
|
and will attempt to use the current CMS `Member` session for authentication context.
|
||||||
|
|
||||||
|
**If you are using the default session-based authentication, please be sure that you have
|
||||||
|
the [CSRF Middleware](csrf_protection) enabled. (It is by default).**
|
||||||
|
|
||||||
|
### HTTP basic authentication
|
||||||
|
|
||||||
|
Silverstripe CMS has built-in support for [HTTP basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
|
||||||
|
There is a `BasicAuthAuthenticator` which is configured for GraphQL by default, but
|
||||||
|
will only activate when required. It is kept separate from the SilverStripe CMS
|
||||||
|
authenticator because GraphQL needs to use the successfully authenticated member
|
||||||
|
for CMS permission filtering, whereas the global `BasicAuth` does not log the
|
||||||
|
member in or use it for model security.
|
||||||
|
|
||||||
|
When using HTTP basic authentication, you can feel free to remove the [CSRF Middleware](csrf_protection),
|
||||||
|
as it just adds unnecessary overhead to the request.
|
||||||
|
|
||||||
|
#### In GraphiQL
|
||||||
|
|
||||||
|
If you want to add basic authentication support to your GraphQL requests you can
|
||||||
|
do so by adding a custom `Authorization` HTTP header to your GraphiQL requests.
|
||||||
|
|
||||||
|
If you are using the [GraphiQL macOS app](https://github.com/skevy/graphiql-app)
|
||||||
|
this can be done from "Edit HTTP Headers". The `/dev/graphiql` implementation
|
||||||
|
does not support custom HTTP headers at this point.
|
||||||
|
|
||||||
|
Your custom header should follow the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Key: Value
|
||||||
|
Authorization: Basic aGVsbG86d29ybGQ=
|
||||||
|
```
|
||||||
|
|
||||||
|
`Basic` is followed by a [base64 encoded](https://en.wikipedia.org/wiki/Base64)
|
||||||
|
combination of your username, colon and password. The above example is `hello:world`.
|
||||||
|
|
||||||
|
**Note:** Authentication credentials are transferred in plain text when using HTTP
|
||||||
|
basic authentication. We strongly recommend using TLS for non-development use.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
php -r 'echo base64_encode("hello:world");'
|
||||||
|
# aGVsbG86d29ybGQ=
|
||||||
|
```
|
||||||
|
|
||||||
|
### Defining your own authenticators
|
||||||
|
|
||||||
|
You will need to define the class under `SilverStripe\GraphQL\Auth\Handlers.authenticators`.
|
||||||
|
You can optionally provide a `priority` number if you want to control which
|
||||||
|
Authenticator is used when multiple are defined (higher priority returns first).
|
||||||
|
|
||||||
|
Authenticator classes will need to implement the `SilverStripe\GraphQL\Auth\AuthenticatorInterface`
|
||||||
|
interface, which requires you to define an `authenticate` method to return a Member, or false, and
|
||||||
|
and `isApplicable` method which tells the `Handler` whether or not this authentication method
|
||||||
|
is applicable in the current request context (provided as an argument).
|
||||||
|
|
||||||
|
Here's an example for implementing HTTP basic authentication:
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
Note that basic auth is enabled by default.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Auth\Handler:
|
||||||
|
authenticators:
|
||||||
|
- class: SilverStripe\GraphQL\Auth\BasicAuthAuthenticator
|
||||||
|
priority: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
title: Cross-Origin Resource Sharing (CORS)
|
||||||
|
summary: Ensure that requests to your API come from a whitelist of origins
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security & best practices
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Cross-Origin Resource Sharing (CORS)
|
||||||
|
|
||||||
|
By default [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) is disabled in the GraphQL Server. This can be easily enabled via YAML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Controller:
|
||||||
|
cors:
|
||||||
|
Enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you have enabled CORS you can then control four new headers in the HTTP Response.
|
||||||
|
|
||||||
|
1. **Access-Control-Allow-Origin.**
|
||||||
|
|
||||||
|
This lets you define which domains are allowed to access your GraphQL API. There are
|
||||||
|
4 options:
|
||||||
|
|
||||||
|
* **Blank**:
|
||||||
|
Deny all domains (except localhost)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Origin:
|
||||||
|
```
|
||||||
|
|
||||||
|
* **'\*'**:
|
||||||
|
Allow requests from all domains.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Origin: '*'
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Single Domain**:
|
||||||
|
|
||||||
|
Allow requests from one specific external domain.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Origin: 'my.domain.com'
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Multiple Domains**:
|
||||||
|
|
||||||
|
Allow requests from multiple specified external domains.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Origin:
|
||||||
|
- 'my.domain.com'
|
||||||
|
- 'your.domain.org'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Access-Control-Allow-Headers.**
|
||||||
|
|
||||||
|
Access-Control-Allow-Headers is part of a CORS 'pre-flight' request to identify
|
||||||
|
what headers a CORS request may include. By default, the GraphQL server enables the
|
||||||
|
`Authorization` and `Content-Type` headers. You can add extra allowed headers that
|
||||||
|
your GraphQL may need by adding them here. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Headers: 'Authorization, Content-Type, Content-Language'
|
||||||
|
```
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
If you add extra headers to your GraphQL server, you will need to write a
|
||||||
|
custom resolver function to handle the response.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
3. **Access-Control-Allow-Methods.**
|
||||||
|
|
||||||
|
This defines the HTTP request methods that the GraphQL server will handle. By
|
||||||
|
default this is set to `GET, PUT, OPTIONS`. Again, if you need to support extra
|
||||||
|
methods you will need to write a custom resolver to handle this. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Methods: 'GET, PUT, DELETE, OPTIONS'
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access-Control-Max-Age.**
|
||||||
|
|
||||||
|
Sets the maximum cache age (in seconds) for the CORS pre-flight response. When
|
||||||
|
the client makes a successful OPTIONS request, it will cache the response
|
||||||
|
headers for this specified duration. If the time expires or the required
|
||||||
|
headers are different for a new CORS request, the client will send a new OPTIONS
|
||||||
|
pre-flight request to ensure it still has authorisation to make the request.
|
||||||
|
This is set to 86400 seconds (24 hours) by default but can be changed in YAML as
|
||||||
|
in this example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Max-Age: 600
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Access-Control-Allow-Credentials.**
|
||||||
|
|
||||||
|
When a request's credentials mode (Request.credentials) is "include", browsers
|
||||||
|
will only expose the response to frontend JavaScript code if the
|
||||||
|
Access-Control-Allow-Credentials value is true.
|
||||||
|
|
||||||
|
The Access-Control-Allow-Credentials header works in conjunction with the
|
||||||
|
XMLHttpRequest.withCredentials property or with the credentials option in the
|
||||||
|
Request() constructor of the Fetch API. For a CORS request with credentials,
|
||||||
|
in order for browsers to expose the response to frontend JavaScript code, both
|
||||||
|
the server (using the Access-Control-Allow-Credentials header) and the client
|
||||||
|
(by setting the credentials mode for the XHR, Fetch, or Ajax request) must
|
||||||
|
indicate that they’re opting in to including credentials.
|
||||||
|
|
||||||
|
This is set to empty by default but can be changed in YAML as in this example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Allow-Credentials: 'true'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apply a CORS config to all GraphQL endpoints
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
## CORS Config
|
||||||
|
SilverStripe\GraphQL\Controller:
|
||||||
|
cors:
|
||||||
|
Enabled: true
|
||||||
|
Allow-Origin: 'silverstripe.org'
|
||||||
|
Allow-Headers: 'Authorization, Content-Type'
|
||||||
|
Allow-Methods: 'GET, POST, OPTIONS'
|
||||||
|
Allow-Credentials: 'true'
|
||||||
|
Max-Age: 600 # 600 seconds = 10 minutes.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apply a CORS config to a single GraphQL endpoint
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
## CORS Config
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Controller.default
|
||||||
|
properties:
|
||||||
|
corsConfig:
|
||||||
|
Enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: CSRF protection
|
||||||
|
summary: Protect destructive actions from cross-site request forgery
|
||||||
|
---
|
||||||
|
# Security & best practices
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## CSRF tokens (required for mutations)
|
||||||
|
|
||||||
|
Even if your graphql endpoints are behind authentication, it is still possible for unauthorised
|
||||||
|
users to access that endpoint through a [CSRF exploitation](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)). This involves
|
||||||
|
forcing an already authenticated user to access an HTTP resource unknowingly (e.g. through a fake image), thereby hijacking the user's
|
||||||
|
session.
|
||||||
|
|
||||||
|
In the absence of a token-based authentication system, like OAuth, the best countermeasure to this
|
||||||
|
is the use of a CSRF token for any requests that destroy or mutate data.
|
||||||
|
|
||||||
|
By default, this module comes with a `CSRFMiddleware` implementation that forces all mutations to check
|
||||||
|
for the presence of a CSRF token in the request. That token must be applied to a header named` X-CSRF-TOKEN`.
|
||||||
|
|
||||||
|
In SilverStripe, CSRF tokens are most commonly stored in the session as `SecurityID`, or accessed through
|
||||||
|
the `SecurityToken` API, using `SecurityToken::inst()->getValue()`.
|
||||||
|
|
||||||
|
Queries do not require CSRF tokens.
|
||||||
|
|
||||||
|
### Disabling CSRF protection (for token-based authentication only)
|
||||||
|
|
||||||
|
If you are using HTTP basic authentication or a token-based system like OAuth or [JWT](https://github.com/Firesphere/silverstripe-graphql-jwt),
|
||||||
|
you will want to remove the CSRF protection, as it just adds unnecessary overhead. You can do this by setting
|
||||||
|
the middleware to `false`.
|
||||||
|
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
|
||||||
|
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
|
||||||
|
properties:
|
||||||
|
Middlewares:
|
||||||
|
csrf: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: Strict HTTP method checking
|
||||||
|
summary: Ensure requests are GET or POST
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security & best practices
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Strict HTTP Method Checking
|
||||||
|
|
||||||
|
According to GraphQL best practices, mutations should be done over `POST`, while queries have the option
|
||||||
|
to use either `GET` or `POST`. By default, this module enforces the `POST` request method for all mutations.
|
||||||
|
|
||||||
|
To disable that requirement, you can remove the `HTTPMethodMiddleware` from your `Manager` implementation.
|
||||||
|
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
|
||||||
|
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
|
||||||
|
properties:
|
||||||
|
Middlewares:
|
||||||
|
httpMethod: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Security & best practices
|
||||||
|
icon: user-lock
|
||||||
|
summary: A guide to keeping your GraphQL API secure and accessible
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security and best practices
|
||||||
|
|
||||||
|
In this section we'll cover several options you have for keeping your GraphQL API secure and compliant
|
||||||
|
with best practices. Some of these tools require configuration, while others come pre-installed.
|
||||||
|
|
||||||
|
[CHILDREN]
|
||||||
|
|
||||||
|
[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]
|
153
docs/en/02_Developer_Guides/19_GraphQL/05_plugins/01_overview.md
Normal file
153
docs/en/02_Developer_Guides/19_GraphQL/05_plugins/01_overview.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
title: What are plugins?
|
||||||
|
summary: An overview of how plugins work with the GraphQL schema
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## What are plugins?
|
||||||
|
|
||||||
|
Plugins are used to distribute reusable functionality across your schema. Some examples of commonly used plugins include:
|
||||||
|
|
||||||
|
* Adding versioning arguments to versioned DataObjects
|
||||||
|
* Adding a custom filter/sort arguments to DataObject queries
|
||||||
|
* Adding a one-off `VerisionedStage` enum to the schema
|
||||||
|
* Ensuring `Member` is in the schema
|
||||||
|
* And many more...
|
||||||
|
|
||||||
|
### Default plugins
|
||||||
|
|
||||||
|
By default, all schemas ship with some plugins installed that will benefit most use cases:
|
||||||
|
|
||||||
|
* The `DataObject` model (i.e. any dataobject based type) has:
|
||||||
|
* An `inheritance` plugin that builds the `__extends` field, and merges ancestral fields.
|
||||||
|
* An `inheritedPlugins` plugin (a bit meta!) that merges plugins from ancestral types into descendants.
|
||||||
|
installed).
|
||||||
|
* The `read` and `readOne` operations have:
|
||||||
|
* A `canView` plugin for hiding records that do not pass a `canView()` check
|
||||||
|
* The `read` operation has:
|
||||||
|
* A `paginateList` plugin for adding pagination arguments and types (e.g. `nodes`)
|
||||||
|
|
||||||
|
In addition to the above, the `default` schema specifically ships with an even richer set of default
|
||||||
|
plugins, including:
|
||||||
|
|
||||||
|
* A `versioning` plugin that adds `version` fields to the dataobject type (if `silverstripe/versioned` is installed)
|
||||||
|
* A `readVersion` plugin (if `silverstripe/versioned` is installed) that allows versioned operations on
|
||||||
|
`read` and `readOne` queries.
|
||||||
|
* A `filter` plugin for filtering queries (adds a `filter` argument)
|
||||||
|
* A `sort` plugin for sorting queries (adds a `sort` argument)
|
||||||
|
|
||||||
|
|
||||||
|
All of these are defined in the `modelConfig` section of the schema (see [configuring your schema](../getting_started/configuring_your_schema)). For reference, see the graphql configuration in `silverstripe/admin`, which applies
|
||||||
|
these default plugins to the `default` schema.
|
||||||
|
|
||||||
|
#### Overriding default plugins
|
||||||
|
You can override default plugins generically in the `modelConfig` section.
|
||||||
|
|
||||||
|
**app/_graphql/modelConfig.yml**
|
||||||
|
```yaml
|
||||||
|
DataObject:
|
||||||
|
plugins:
|
||||||
|
inheritance: false # No dataobject models get this plugin unless opted into
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
paginateList: false # No dataobject models have paginated read operations unless opted into
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override default plugins on your specific dataobject type and these changes will be inherited by descendants.
|
||||||
|
|
||||||
|
**app/_graphql/models.yml**
|
||||||
|
```yaml
|
||||||
|
Page:
|
||||||
|
plugins:
|
||||||
|
inheritance: false
|
||||||
|
MyProject\MyCustomPage: {} # now has no inheritance plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Likewise, you can do the same for operations:
|
||||||
|
|
||||||
|
**app/_graphql/models.yml**
|
||||||
|
```yaml
|
||||||
|
Page:
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
readVersion: false
|
||||||
|
MyProject\MyCustomPage:
|
||||||
|
operations:
|
||||||
|
read: true # has no readVersion plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### What plugins must do
|
||||||
|
|
||||||
|
There isn't a huge API surface to a plugin. They just have to:
|
||||||
|
|
||||||
|
* Implement at least one of several plugin interfaces
|
||||||
|
* Declare an identifier
|
||||||
|
* Apply themselves to the schema with the `apply(Schema $schema)` method
|
||||||
|
* Be registered with the `PluginRegistry`
|
||||||
|
|
||||||
|
|
||||||
|
### Available plugin interfaces
|
||||||
|
|
||||||
|
Plugin interfaces are all found in the namespace `SilverStripe\GraphQL\Schema\Interfaces`
|
||||||
|
|
||||||
|
* `SchemaUpdater`: Make a one-off, context-free update to the schema
|
||||||
|
* `QueryPlugin`: Update a generic query
|
||||||
|
* `MutationPlugin`: Update a generic mutation
|
||||||
|
* `TypePlugin`: Update a generic type
|
||||||
|
* `FieldPlugin`: Update a field on a generic type
|
||||||
|
* `ModelQueryPlugin`: Update queries generated by a model, e.g. `readPages`
|
||||||
|
* `ModelMutationPlugin`: Update mutations generated by a model, e.g. `createPage`
|
||||||
|
* `ModelTypePlugin`: Update types that are generated by a model
|
||||||
|
* `ModelFieldPlugin`: Update a field on types generated by a model
|
||||||
|
|
||||||
|
Wow, that's a lot of interfaces, right? This is owing mostly to issues around strict typing between interfaces,
|
||||||
|
and allows for a more expressive developer experience. Almost all of these interfaces have the same requirements,
|
||||||
|
just for different types. It's pretty easy to navigate if you know what you want to accomplish.
|
||||||
|
|
||||||
|
### Registering plugins
|
||||||
|
|
||||||
|
Plugins have to be registered with Injector.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
|
||||||
|
constructor:
|
||||||
|
myPlugin: '%$MyProject\Plugins\MyPlugin'
|
||||||
|
```
|
||||||
|
|
||||||
|
[info]
|
||||||
|
The key `myPlugin` is arbitrary. The identifier of the plugin is obtained procedurally.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
### Resolver middleware and afterware
|
||||||
|
|
||||||
|
The real power of plugins is the ability to distribute not just configuration across the schema, but
|
||||||
|
more importantly, _functionality_.
|
||||||
|
|
||||||
|
Fields have their own resolvers already, so we can't really get into those to change
|
||||||
|
their functionality without a massive hack. This is where the idea of **resolver middleware** and
|
||||||
|
**resolver afterware** comes in really useful.
|
||||||
|
|
||||||
|
**Resolver middleware** runs _before_ the field's assigned resolver
|
||||||
|
**Resolver afterware** runs _after_ the field's assigned resolver
|
||||||
|
|
||||||
|
Middlewares and afterwares are pretty straightforward. They get the same `$args`, `$context`, and `$info`
|
||||||
|
parameters as the assigned resolver, but the first argument, `$result` is mutated with each resolver.
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
title: Writing a simple plugin
|
||||||
|
summary: In this tutorial, we add a simple plugin for string fields
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Writing a simple plugin
|
||||||
|
|
||||||
|
For this example, we want all `String` fields to have a `truncate` argument that will limit the length of the string
|
||||||
|
in the response.
|
||||||
|
|
||||||
|
Because it applies to fields, we'll want `FieldPlugin` for this.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Truncator implements FieldPlugin
|
||||||
|
{
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return 'truncate';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(Field $field, Schema $schema, array $config = [])
|
||||||
|
{
|
||||||
|
$field->addArg('truncate', 'Int');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we've added an argument to any field that implements the `truncate` plugin. This is good, but it really
|
||||||
|
doesn't save us a whole lot of time. The real value here is that the field will automatically apply the truncation.
|
||||||
|
|
||||||
|
For that, we'll need to augment the resolver with some _afterware_.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function apply(Field $field, Schema $schema, array $config = [])
|
||||||
|
{
|
||||||
|
// Sanity check
|
||||||
|
Schema::invariant(
|
||||||
|
$field->getType() === 'String',
|
||||||
|
'Field %s is not a string. Cannot truncate.',
|
||||||
|
$field->getName()
|
||||||
|
);
|
||||||
|
|
||||||
|
$field->addArg('truncate', 'Int');
|
||||||
|
$field->addResolverAfterware([static::class, 'truncate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function truncate(string $result, array $args): string
|
||||||
|
{
|
||||||
|
$limit = $args['truncate'] ?? null;
|
||||||
|
if ($limit) {
|
||||||
|
return substr($result, 0, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's register the plugin:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
|
||||||
|
constructor:
|
||||||
|
truncate: '%$MyProject\Plugins\Truncator'
|
||||||
|
```
|
||||||
|
|
||||||
|
And now we can apply it to any string field we want:
|
||||||
|
|
||||||
|
**app/_graphql/types.yml**
|
||||||
|
```yaml
|
||||||
|
Country:
|
||||||
|
name:
|
||||||
|
type: String
|
||||||
|
plugins:
|
||||||
|
truncate: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,295 @@
|
|||||||
|
---
|
||||||
|
title: Writing a complex plugin
|
||||||
|
summary: In this tutorial, we'll create a plugin that affects models, queries, and input types
|
||||||
|
---
|
||||||
|
# Plugins
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Writing a complex plugin
|
||||||
|
|
||||||
|
For this example, we'll imagine that a lot of our DataObjects are geocoded, and this is ostensibly some kind of
|
||||||
|
`DataExtension` that adds lat/lon information to the DataObject, and maybe allows you to ask how close it is to
|
||||||
|
a given lat/lon pair.
|
||||||
|
|
||||||
|
We want any queries using these dataobjects to be able to search within a radius of a given lat/lon.
|
||||||
|
|
||||||
|
To do this, we'll need a few things:
|
||||||
|
|
||||||
|
* DataObjects that are geocodable should always expose their lat/lon fields
|
||||||
|
* `read` operations for DataObjects that are geocodable should include a `within` parameter
|
||||||
|
* An input type for this lat/lon parameter should be globally available to the schema
|
||||||
|
* A resolver should automatically filter the result set by proximity.
|
||||||
|
|
||||||
|
Let's get started.
|
||||||
|
|
||||||
|
### Step 1: Ensure DataObjects expose lat/lon fields
|
||||||
|
|
||||||
|
Since we're dealing with DataObjects, we'll need a `ModelTypePlugin`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class GeocodableDataObject implements ModelTypePlugin
|
||||||
|
{
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return 'geocode';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(ModelType $type, Schema $schema, array $config = []): void
|
||||||
|
{
|
||||||
|
$class = $type->getModel()->getSourceClass();
|
||||||
|
|
||||||
|
// sanity check that this is a dataobject
|
||||||
|
Schema::invariant(
|
||||||
|
is_subclass_of($class, DataObject::class),
|
||||||
|
'The %s plugin can only be applied to types generated by %s models',
|
||||||
|
__CLASS__,
|
||||||
|
DataObject::class
|
||||||
|
);
|
||||||
|
|
||||||
|
// only apply the plugin to geocodable DataObjects
|
||||||
|
if (!Extensible::has_extension($class, Geocodable::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type->addField('Lat')
|
||||||
|
->addField('Lon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now all dataobjects that have the extension will be forced to expose their lat/lon fields.
|
||||||
|
|
||||||
|
### Step 2: Add a new parameter to the queries
|
||||||
|
|
||||||
|
We want any `readXXX` query to include a `within` parameter if it's for a geocodable DataObject.
|
||||||
|
For this, we're going to use `ModelQueryPlugin`, because this is for queries generated by a model.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class GeocodableQuery implements ModelQueryPlugin
|
||||||
|
{
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return 'geocodableQuery';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(ModelQuery $query, Schema $schema, array $config = []): void
|
||||||
|
{
|
||||||
|
$class = $query->getModel()->getSourceClass();
|
||||||
|
// Only apply to geocodable objects
|
||||||
|
if (!Extensible::has_extension($class, Geocodable::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->addArg('within', 'SearchRadiusInput');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now our read queries will have a new parameter:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query readEvents(within: ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
But we're not done yet! What is `SearchRadiusInput`? We haven't defined that yet. Ideally, we want our query
|
||||||
|
to look like this:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
readEvents(within: {
|
||||||
|
lat: 123.123456,
|
||||||
|
lon: 123.123456,
|
||||||
|
proximity: 500,
|
||||||
|
unit: METER
|
||||||
|
}) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
lat
|
||||||
|
lon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Step 3: Adding an input type
|
||||||
|
|
||||||
|
We'll need this `SearchRadiusInput` to be shared across queries. It's not specific to any DataObject.
|
||||||
|
For this, we can use `SchemaUpdater`. For tidiness, let's just to this in the same `GeocodeableQuery`
|
||||||
|
class, since they share concerns.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class GeocodableQuery implements ModelQueryPlugin, SchemUpdater
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
|
||||||
|
public static function updateSchema(Schema $schema): void
|
||||||
|
{
|
||||||
|
$unitType = Enum::create('Unit', [
|
||||||
|
'METER' => 'METER',
|
||||||
|
'KILOMETER' => 'KILOMETER',
|
||||||
|
'FOOT' => 'FOOT',
|
||||||
|
'MILE' => 'MILE',
|
||||||
|
]);
|
||||||
|
$radiusType = InputType::create('SearchRadiusInput')
|
||||||
|
->setFields([
|
||||||
|
'lat' => 'Float!',
|
||||||
|
'lon' => 'Float!',
|
||||||
|
'proximity' => 'Int!',
|
||||||
|
'unit' => 'Unit!'
|
||||||
|
]);
|
||||||
|
$schema->addType($unitType);
|
||||||
|
$schema->addType($unitType);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So now we can run queries with these parameters, but we need to somehow apply it to the result set.
|
||||||
|
|
||||||
|
### Step 4: Add a resolver to apply the filter
|
||||||
|
|
||||||
|
All these DataObjects have their own resolvers already, so we can't really get into those to change
|
||||||
|
their functionality without a massive hack. This is where the idea of **resolver middleware** and
|
||||||
|
**resolver afterware** comes in really useful.
|
||||||
|
|
||||||
|
**Resolver middleware** runs _before_ the operation's assigned resolver
|
||||||
|
**Resolver afterware** runs _after_ the operation's assigned resolver
|
||||||
|
|
||||||
|
Middlewares and afterwares are pretty straightforward. They get the same `$args`, `$context`, and `$info`
|
||||||
|
parameters as the assigned resolver, but the first argument, `$result` is mutated with each resolver.
|
||||||
|
|
||||||
|
In this case, we're going to be filtering our DataList procedurally, transforming it into an array,
|
||||||
|
so we need to know that things like filters and sort have already been applied, as they expect a `DataList`
|
||||||
|
instance. So we'll need to do this fairly late in the chain. Afterware makes the most sense.
|
||||||
|
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function apply(ModelQuery $query, Schema $schema, array $config = []): void
|
||||||
|
{
|
||||||
|
$class = $query->getModel()->getSourceClass();
|
||||||
|
// Only apply to geocodable objects
|
||||||
|
if (!Extensible::has_extension($class, Geocodable::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->addArg('within', 'SearchRadiusInput');
|
||||||
|
$query->addResolverAfterware([static::class, 'applyRadius']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function applyRadius($result, array $args): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
// imaginary class
|
||||||
|
$proximity = new Proximity($args['unit'], $args['lat'], $args['lon']);
|
||||||
|
foreach ($result as $record) {
|
||||||
|
if ($proximity->isWithin($args['proximity'], $record->Lat, $record->Lon)) {
|
||||||
|
$results[] = $record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Looking good!
|
||||||
|
|
||||||
|
But there's still one little gotcha. This is likely to be run after pagination has been executed, so our
|
||||||
|
`$result` parameter is probably an array of `edges`, `nodes`, etc.
|
||||||
|
|
||||||
|
**app/src/Resolvers/MyResolver.php**
|
||||||
|
```php
|
||||||
|
public static function applyRadius($result, array $args)
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
// imaginary class
|
||||||
|
$proximity = new Proximity($args['unit'], $args['lat'], $args['lon']);
|
||||||
|
foreach ($result['nodes'] as $record) {
|
||||||
|
if ($proximity->isWithin($args['proximity'], $record->Lat, $record->Lon)) {
|
||||||
|
$results[] = $record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'edges' => $results,
|
||||||
|
'nodes' => $results,
|
||||||
|
'pageInfo' => $result['pageInfo'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If we added this plugin in middleware rather than afterware,
|
||||||
|
we could filter the result set by a list of `IDs` early on, which would allow us to keep
|
||||||
|
a `DataList` throughout the whole cycle, but this would force us to loop over an
|
||||||
|
unlimited result set, and that's never a good idea.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
If you've picked up on the inconsistency that the `pageInfo` property is now inaccurate, this is a long-standing
|
||||||
|
issue with doing post-query filters. Ideally, any middleware that filters a DataList should do it at the query level,
|
||||||
|
but that's not always possible.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
### Step 5: Register the plugins
|
||||||
|
|
||||||
|
Back to Injector:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Schema\Registry\PluginRegistry:
|
||||||
|
constructor:
|
||||||
|
geocodeQuery: '%$MyProject\Plugins\GeocodableQuery'
|
||||||
|
geocodeDataObject: '%$MyProject\Plugins\GeocodableDataObject'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Apply the plugins
|
||||||
|
|
||||||
|
We can apply the plugins to queries and dataobjects one of two ways:
|
||||||
|
|
||||||
|
* Add them on a case-by-case basis to our config
|
||||||
|
* Add them as default plugins so that we never have to worry about it.
|
||||||
|
|
||||||
|
Let's look at each approach:
|
||||||
|
|
||||||
|
#### Case-by-case
|
||||||
|
|
||||||
|
**app/_graphql/models.yml**
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\Event:
|
||||||
|
plugins:
|
||||||
|
geocode: true
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
operations:
|
||||||
|
read:
|
||||||
|
plugins:
|
||||||
|
geocodeableQuery: true
|
||||||
|
```
|
||||||
|
|
||||||
|
This can get pretty verbose, so you might just want to register them as default plugins for all DataObjects
|
||||||
|
and their `read` operations.
|
||||||
|
|
||||||
|
#### Apply by default
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# apply the DataObject plugin
|
||||||
|
SilverStripe\GraphQL\Schema\DataObject\DataObjectModel:
|
||||||
|
default_plugins:
|
||||||
|
geocode: true
|
||||||
|
# apply the query plugin
|
||||||
|
SilverStripe\GraphQL\Schema\DataObject\ReadCreator:
|
||||||
|
default_plugins:
|
||||||
|
geocodableQuery: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
22
docs/en/02_Developer_Guides/19_GraphQL/05_plugins/index.md
Normal file
22
docs/en/02_Developer_Guides/19_GraphQL/05_plugins/index.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Plugins
|
||||||
|
summary: Learn what plugins are and how you can use them to extend your schema
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
|
||||||
|
Plugins play a critical role in distributing reusable functionality across your schema. They can apply to just about
|
||||||
|
everything loaded into the schema, including types, fields, queries, mutations, and even specifically to model types
|
||||||
|
and their fields and operations.
|
||||||
|
|
||||||
|
Let's dive in!
|
||||||
|
|
||||||
|
[CHILDREN]
|
||||||
|
|
||||||
|
[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]
|
@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
title: Adding a custom model
|
||||||
|
summary: Add a new class-backed type beyond DataObject
|
||||||
|
---
|
||||||
|
# Extending the Schema
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Adding a custom model
|
||||||
|
|
||||||
|
The only point of contact the `silverstripe-graphql` schema has with
|
||||||
|
Silverstripe specifically is through the `DataObjectModel` adapter
|
||||||
|
and its associated plugins. This is important, because it means you
|
||||||
|
can plug in any schema-aware class as a model, and it will be afforded
|
||||||
|
all the same features as DataObjects.
|
||||||
|
|
||||||
|
It is, however, hard to imagine a model-driven type that isn't
|
||||||
|
related to an ORM, so we'll keep this section simple and just describe
|
||||||
|
what the requirements are rather than think up an outlandish example
|
||||||
|
of what a non-DataObject model might be.
|
||||||
|
|
||||||
|
### SchemaModelInterface
|
||||||
|
|
||||||
|
Models must implement the `SchemaModelInterface`, which has some
|
||||||
|
hefty requirements. Let's walk through them:
|
||||||
|
|
||||||
|
* `getIdentifier(): string`: A unique identifier for this model type,
|
||||||
|
e.g. 'DataObject'
|
||||||
|
* `hasField(string $fieldName): bool`: Return true if `$fieldName` exists
|
||||||
|
on the model
|
||||||
|
* `getTypeForField(string $fieldName): ?string`: Given a field name,
|
||||||
|
infer the type. If the field doesn't exist, return `null`
|
||||||
|
* `getTypeName(): string`: Get the name of the type (i.e. based on
|
||||||
|
the source class)
|
||||||
|
* `getDefaultResolver(?array $context = []): ResolverReference`:
|
||||||
|
Get the generic resolver that should be used for types that are built
|
||||||
|
with this model.
|
||||||
|
* `getSourceClass(): string`: Get the name of the class that builds
|
||||||
|
the type, e.g. `MyDataObject`
|
||||||
|
* `getAllFields(): array`: Get all availble fields on the object
|
||||||
|
* `getModelField(string $fieldName): ?ModelType`: For nested fields.
|
||||||
|
If a field resolves to another model (e.g. has_one), return that
|
||||||
|
model type.
|
||||||
|
|
||||||
|
In addition, models may want to implement:
|
||||||
|
|
||||||
|
* `OperationProvider` (if your model creates operations, like
|
||||||
|
read, create, etc)
|
||||||
|
* `DefaultFieldsProvider` (if your model provides a default list
|
||||||
|
of fields, e.g. `id`)
|
||||||
|
* `DefaultPluginProvider` (if your model can supply a default set
|
||||||
|
of plugins, e.g. `default_plugins` on `DataObjectModel`)
|
||||||
|
|
||||||
|
This is all a lot to take in out of context. A good exercise would be
|
||||||
|
to look through how `DataObjectModel` implements all these methods.
|
||||||
|
|
||||||
|
### SchemaModelCreatorInterface
|
||||||
|
|
||||||
|
Given a class name, create an instance of `SchemaModelInterface`.
|
||||||
|
This layer of abstraction is necessary because we can't assume that
|
||||||
|
all `SchemaModelInterfaces` will accept a class name in their
|
||||||
|
constructors.
|
||||||
|
|
||||||
|
Implementors of this interface just need to be able to report
|
||||||
|
whether they apply to a given class and create a model given a
|
||||||
|
class name.
|
||||||
|
|
||||||
|
Look at the DataObjectModel implementation:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class ModelCreator implements SchemaModelCreatorInterface
|
||||||
|
{
|
||||||
|
use Injectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function appliesTo(string $class): bool
|
||||||
|
{
|
||||||
|
return is_subclass_of($class, DataObject::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @return SchemaModelInterface
|
||||||
|
*/
|
||||||
|
public function createModel(string $class): SchemaModelInterface
|
||||||
|
{
|
||||||
|
return DataObjectModel::create($class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering your model creator
|
||||||
|
|
||||||
|
Just add it to the registry:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Schema\Registry\SchemaModelCreatorRegistry:
|
||||||
|
constructor:
|
||||||
|
dataobject: '%$SilverStripe\GraphQL\Schema\DataObject\ModelCreator'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Adding a custom operation
|
||||||
|
summary: Add a new operation for model types
|
||||||
|
---
|
||||||
|
# Extending the schema
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Adding a custom operation
|
||||||
|
|
||||||
|
By default, we get basic operations for our models, like `read`, `create`,
|
||||||
|
`update`, and `delete`, but we can add to this list by creating
|
||||||
|
an implementation of `OperationProvider` and registering it.
|
||||||
|
|
||||||
|
Let's build a new operation that **duplicates** DataObjects.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class DuplicateCreator implements OperationCreator
|
||||||
|
{
|
||||||
|
public function createOperation(
|
||||||
|
SchemaModelInterface $model,
|
||||||
|
string $typeName,
|
||||||
|
array $config = []
|
||||||
|
): ?ModelOperation
|
||||||
|
{
|
||||||
|
$mutationName = 'duplicate' . ucfirst(Schema::pluralise($typeName));
|
||||||
|
|
||||||
|
return ModelMutation::create($model, $mutationName)
|
||||||
|
->setType($typeName)
|
||||||
|
->addArg('id', 'ID!')
|
||||||
|
->setDefaultResolver([static::class, 'resolve'])
|
||||||
|
->setResolverContext([
|
||||||
|
'dataClass' => $model->getSourceClass(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We add **resolver context** to the mutation because we need to know
|
||||||
|
what class to duplicate, but we need to make sure we still have a
|
||||||
|
static function.
|
||||||
|
|
||||||
|
The signature for resolvers with context is:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function (array $context): Closure;
|
||||||
|
```
|
||||||
|
|
||||||
|
We use the context to pass to a function that we'll create dynamically.
|
||||||
|
Let's add that now.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function resolve(array $resolverContext = []): Closure
|
||||||
|
{
|
||||||
|
$dataClass = $resolverContext['dataClass'] ?? null;
|
||||||
|
return function ($obj, array $args) use ($dataClass) {
|
||||||
|
if (!$dataClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DataObject::get_by_id($dataClass, $args['id'])
|
||||||
|
->duplicate();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, just add the operation to the `DataObjectModel` configuration
|
||||||
|
to make it available to all DataObject types.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Schema\DataObject\DataObjectModel:
|
||||||
|
operations:
|
||||||
|
duplicate: 'MyProject\Operations\DuplicateCreator'
|
||||||
|
```
|
||||||
|
|
||||||
|
And use it:
|
||||||
|
|
||||||
|
**app/_graphql/models.yml**
|
||||||
|
```yaml
|
||||||
|
MyProject\Models\MyDataObject:
|
||||||
|
fields: '*'
|
||||||
|
operations:
|
||||||
|
read: true
|
||||||
|
duplicate: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: Adding middleware
|
||||||
|
summary: Add middleware to to extend query execution
|
||||||
|
---
|
||||||
|
# Extending the schema
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Adding middleware
|
||||||
|
|
||||||
|
Middleware is any piece of functionality that is interpolated into
|
||||||
|
a larger process. A key feature of middleware is that it can be used
|
||||||
|
with other middlewares in sequence and not have to worry about the order
|
||||||
|
of execution.
|
||||||
|
|
||||||
|
In `silverstripe-graphql`, middleware is used for query execution,
|
||||||
|
but could ostensibly be used elsewhere too if the API ever accomodates
|
||||||
|
such an expansion.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
The middleware API in the silverstripe-graphql module is separate from other common middleware
|
||||||
|
APIs in Silverstripe CMS, such as HTTPMiddleware.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
The signature for middleware is pretty simple:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function process(array $params, callable $next)
|
||||||
|
```
|
||||||
|
|
||||||
|
`$params` is an arbitrary array of data, much like an event object
|
||||||
|
passed to an event handler. The `$next` parameter refers to the next
|
||||||
|
middleware in the chain.
|
||||||
|
|
||||||
|
Let's write a simple middleware that logs our queries as they come in.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class LoggingMiddleware implements Middleware
|
||||||
|
{
|
||||||
|
public function process(array $params, callable $next)
|
||||||
|
{
|
||||||
|
$query = $params['query'];
|
||||||
|
Injector::inst()->get(LoggerInterface::class)
|
||||||
|
->info('Query executed: ' . $query);
|
||||||
|
|
||||||
|
// Hand off execution to the next middleware
|
||||||
|
return $next($params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can register the middleware with our query handler:
|
||||||
|
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
|
||||||
|
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
|
||||||
|
properties:
|
||||||
|
Middlewares:
|
||||||
|
logging: '%$MyProject\Middleware\LoggingMiddleware'
|
||||||
|
```
|
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: The global schema
|
||||||
|
summary: How to push modifications to every schema in the project
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extending the schema
|
||||||
|
|
||||||
|
[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 global schema
|
||||||
|
|
||||||
|
Developers of thirdparty modules that influence graphql schemas may want to take advantage
|
||||||
|
of the _global schema_. This is a pseudo-schema that will merge itself with all other schemas
|
||||||
|
that have been defined. A good use case is in the `silverstripe/versioned` module, where it
|
||||||
|
is critical that all schemas can leverage its schema modifications.
|
||||||
|
|
||||||
|
The global schema is named `*`.
|
||||||
|
|
||||||
|
**app/_config/graphql.yml**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
'*':
|
||||||
|
enums:
|
||||||
|
VersionedStage:
|
||||||
|
DRAFT: DRAFT
|
||||||
|
LIVE: LIVE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Further reading
|
||||||
|
|
||||||
|
[CHILDREN]
|
18
docs/en/02_Developer_Guides/19_GraphQL/06_extending/index.md
Normal file
18
docs/en/02_Developer_Guides/19_GraphQL/06_extending/index.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: Extending the schema
|
||||||
|
summary: Add new functionality to the schema
|
||||||
|
---
|
||||||
|
|
||||||
|
In this section of the documentation, we'll look at some advanced
|
||||||
|
features for developers who want to extend their GraphQL server
|
||||||
|
using custom models, middleware, and new operations.
|
||||||
|
|
||||||
|
[CHILDREN]
|
||||||
|
|
||||||
|
[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]
|
170
docs/en/02_Developer_Guides/19_GraphQL/07_tips_and_tricks.md
Normal file
170
docs/en/02_Developer_Guides/19_GraphQL/07_tips_and_tricks.md
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
title: Tips & Tricks
|
||||||
|
summary: Miscellaneous useful tips for working with your GraphQL schema
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tips & Tricks
|
||||||
|
|
||||||
|
[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]
|
||||||
|
|
||||||
|
## Persisting queries
|
||||||
|
|
||||||
|
A common pattern in GraphQL APIs is to store queries on the server by an identifier. This helps save
|
||||||
|
on bandwidth, as the client need not put a fully expressed query in the request body, but rather a
|
||||||
|
simple identifier. Also, it allows you to whitelist only specific query IDs, and block all other ad-hoc,
|
||||||
|
potentially malicious queries, which adds an extra layer of security to your API, particularly if it's public.
|
||||||
|
|
||||||
|
To implement persisted queries, you need an implementation of the
|
||||||
|
`SilverStripe\GraphQL\PersistedQuery\PersistedQueryMappingProvider` interface. By default, three are provided,
|
||||||
|
which cover most use cases:
|
||||||
|
|
||||||
|
* `FileProvider`: Store your queries in a flat JSON file on the local filesystem.
|
||||||
|
* `HTTPProvider`: Store your queries on a remote server and reference a JSON file by URL.
|
||||||
|
* `JSONStringProvider`: Store your queries as hardcoded JSON
|
||||||
|
|
||||||
|
### Configuring query mapping providers
|
||||||
|
|
||||||
|
All of these implementations can be configured through `Injector`.
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
Note that each schema gets its own set of persisted queries. In these examples, we're using the `default`schema.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
#### FileProvider
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\PersistedQuery\PersistedQueryMappingProvider:
|
||||||
|
class: SilverStripe\GraphQL\PersistedQuery\FileProvider
|
||||||
|
properties:
|
||||||
|
schemaMapping:
|
||||||
|
default: '/var/www/project/query-mapping.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A flat file in the path `/var/www/project/query-mapping.json` should contain something like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"someUniqueID":"query{validateToken{Valid Message Code}}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
The file path must be absolute.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
#### HTTPProvider
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\PersistedQuery\PersistedQueryMappingProvider:
|
||||||
|
class: SilverStripe\GraphQL\PersistedQuery\HTTPProvider
|
||||||
|
properties:
|
||||||
|
schemaMapping:
|
||||||
|
default: 'http://example.com/myqueries.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
A flat file at the URL `http://example.com/myqueries.json` should contain something like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"someUniqueID":"query{readMembers{Name+Email}}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSONStringProvider
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\PersistedQuery\PersistedQueryMappingProvider:
|
||||||
|
class: SilverStripe\GraphQL\PersistedQuery\HTTPProvider
|
||||||
|
properties:
|
||||||
|
schemaMapping:
|
||||||
|
default: '{"myMutation":"mutation{createComment($comment:String!){Comment}}"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The queries are hardcoded into the configuration.
|
||||||
|
|
||||||
|
### Requesting queries by identifier
|
||||||
|
|
||||||
|
To access a persisted query, simply pass an `id` parameter in the request in lieu of `query`.
|
||||||
|
|
||||||
|
`GET http://example.com/graphql?id=someID`
|
||||||
|
|
||||||
|
[notice]
|
||||||
|
Note that if you pass `query` along with `id`, an exception will be thrown.
|
||||||
|
[/notice]
|
||||||
|
|
||||||
|
## Query caching (Caution: EXPERIMENTAL)
|
||||||
|
|
||||||
|
The `QueryCachingMiddleware` class is an experimental cache layer that persists the results of a GraphQL
|
||||||
|
query to limit unnecessary calls to the database. The query cache is automatically expired when any
|
||||||
|
DataObject that it relies on is modified. The entire cache will be discarded on `?flush` requests.
|
||||||
|
|
||||||
|
To implement query caching, add the middleware to your `QueryHandlerInterface`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface.default:
|
||||||
|
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
|
||||||
|
properties:
|
||||||
|
Middlewares:
|
||||||
|
cache: '%$SilverStripe\GraphQL\Middleware\QueryCachingMiddleware'
|
||||||
|
```
|
||||||
|
|
||||||
|
And you will also need to apply an extension to all DataObjects:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SilverStripe\ORM\DataObject:
|
||||||
|
extensions:
|
||||||
|
- SilverStripe\GraphQL\Extensions\QueryRecorderExtension
|
||||||
|
```
|
||||||
|
|
||||||
|
[warning]
|
||||||
|
This feature is experimental, and has not been thoroughly evaluated for security. Use at your own risk.
|
||||||
|
[/warning]
|
||||||
|
|
||||||
|
|
||||||
|
## Schema introspection
|
||||||
|
|
||||||
|
Some GraphQL clients such as [Apollo](http://apollographql.com) require some level of introspection
|
||||||
|
into the schema. While introspection is [part of the GraphQL spec](http://graphql.org/learn/introspection/),
|
||||||
|
this module provides a limited API for fetching it via non-graphql endpoints. By default, the `graphql/`
|
||||||
|
controller provides a `types` action that will return the type schema (serialised as JSON) dynamically.
|
||||||
|
|
||||||
|
*GET http://example.com/graphql/types*
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"data":{
|
||||||
|
"__schema":{
|
||||||
|
"types":[
|
||||||
|
{
|
||||||
|
"kind":"OBJECT",
|
||||||
|
"name":"Query",
|
||||||
|
"possibleTypes":null
|
||||||
|
}
|
||||||
|
// etc ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
As your schema grows, introspecting it dynamically may have a performance hit. Alternatively,
|
||||||
|
if you have the `silverstripe/assets` module installed (as it is in the default SilverStripe installation),
|
||||||
|
GraphQL can cache your schema as a flat file in the `assets/` directory. To enable this, simply
|
||||||
|
set the `cache_types_in_filesystem` setting to `true` on `SilverStripe\GraphQL\Controller`. Once enabled,
|
||||||
|
a `types.graphql` file will be written to your `assets/` directory on `flush`.
|
||||||
|
|
||||||
|
When `cache_types_in_filesystem` is enabled, it is recommended that you remove the extension that
|
||||||
|
provides the dynamic introspection endpoint.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use SilverStripe\GraphQL\Controller;
|
||||||
|
use SilverStripe\GraphQL\Extensions\IntrospectionProvider;
|
||||||
|
|
||||||
|
Controller::remove_extension(IntrospectionProvider::class);
|
||||||
|
```
|
407
docs/en/02_Developer_Guides/19_GraphQL/08_upgrading.md
Normal file
407
docs/en/02_Developer_Guides/19_GraphQL/08_upgrading.md
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
---
|
||||||
|
title: Upgrading from GraphQL 3
|
||||||
|
summary: A high-level view of what you'll need to change when upgrading to GraphQL 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Upgrading from GraphQL 3
|
||||||
|
|
||||||
|
[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 4.0 release of `silverstripe-graphql` underwent a massive set of changes representing an
|
||||||
|
entire rewrite of the module. This was done as part of a year-long plan to improve performance. While
|
||||||
|
there is no specific upgrade path, there are some key things to look out for and general guidelines on how
|
||||||
|
to adapt your code from the 3.x release to 4.x.
|
||||||
|
|
||||||
|
In this section, we'll cover each of these upgrade issues in order of impact.
|
||||||
|
|
||||||
|
## GraphQL schemas require a build step
|
||||||
|
|
||||||
|
The most critical change moving from 3.x to 4.x affects the developer experience.
|
||||||
|
The key to improving performance in GraphQL requests was eliminating the overhead of generating the schema at
|
||||||
|
runtime. This didn't scale. As the GraphQL schema grew, API response latency increase.
|
||||||
|
|
||||||
|
To eliminate this overhead, the GraphQL API relies on **generated code** for the schema. You need to run a
|
||||||
|
task to build it.
|
||||||
|
|
||||||
|
To run the task, use:
|
||||||
|
|
||||||
|
`$ vendor/bin/sake dev/graphql/build schema=mySchema`
|
||||||
|
|
||||||
|
You can also run the task in the browser:
|
||||||
|
|
||||||
|
`http://example.com/dev/graphql/build?schema=mySchema`
|
||||||
|
|
||||||
|
[info]
|
||||||
|
Most of the time, the name of your schema is `default`. If you're editing DataObjects that are accessed
|
||||||
|
with GraphQL in the CMS, you may have to build the `admin` schema as well.
|
||||||
|
[/info]
|
||||||
|
|
||||||
|
This build process is a larger topic with a few more things to be aware of.
|
||||||
|
Check the [building the schema](getting_started/building_the_schema) documentation to learn more.
|
||||||
|
|
||||||
|
|
||||||
|
## The Manager class, the godfather of GraphQL 3, is gone
|
||||||
|
|
||||||
|
`silverstripe-graphql` 3.x relied heavily on the `Manager` class. This became a catch-all that handled
|
||||||
|
registration of types, execution of scaffolding, running queries and middleware, error handling, and more. This
|
||||||
|
class has been broken up into separate concerns:
|
||||||
|
|
||||||
|
* `Schema` <- register your stuff here
|
||||||
|
* `QueryHandlerInterface` <- Handles GraphQL queries. You'll probably never have to touch it.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
**before**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Manager:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
types: {}
|
||||||
|
queries: {}
|
||||||
|
mutations: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
**after**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
src: app/_graphql # A directory of your choice
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the appropriate yaml files to the directory. For more information on this pattern, see
|
||||||
|
the [configuring your schema](01_getting_started/02_configuring_your_schema) section.
|
||||||
|
|
||||||
|
```
|
||||||
|
app/_graphql
|
||||||
|
types.yml
|
||||||
|
queries.yml
|
||||||
|
mutations.yml
|
||||||
|
models.yml
|
||||||
|
enums.yml
|
||||||
|
interfaces.yml
|
||||||
|
unions.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeCreator, QueryCreator, and MutationCreator are gone
|
||||||
|
|
||||||
|
A thorough look at how these classes were being used revealed that they were really just functioning
|
||||||
|
as value objects that basically just created configuration in a static context. That is, they had no
|
||||||
|
real reason to be instance-based. Most of the time, they can easily be ported to configuration.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
|
||||||
|
**before**
|
||||||
|
```php
|
||||||
|
class GroupTypeCreator extends TypeCreator
|
||||||
|
{
|
||||||
|
public function attributes()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'group'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ID' => ['type' => Type::nonNull(Type::id())],
|
||||||
|
'Title' => ['type' => Type::string()],
|
||||||
|
'Description' => ['type' => Type::string()]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**after**
|
||||||
|
|
||||||
|
**app/_graphql/types.yml**
|
||||||
|
```yaml
|
||||||
|
group:
|
||||||
|
fields:
|
||||||
|
ID: ID!
|
||||||
|
Title: String
|
||||||
|
Description: String
|
||||||
|
```
|
||||||
|
|
||||||
|
That's a simple type, and obviously there's a lot more to it than that, but have a look at the
|
||||||
|
[working with generic types](getting_started/working_with_generic_types) section of the documentation.
|
||||||
|
|
||||||
|
## Resolvers must be static callables
|
||||||
|
|
||||||
|
You can no longer use instance methods for resolvers. They can't be easily transformed into generated
|
||||||
|
PHP code in the schema build step. These resolvers should be refactored to use the `static` declaration
|
||||||
|
and moved into a class.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
Move your resolvers into one or many `ResolverProvider` implementations, register them.
|
||||||
|
|
||||||
|
**before**
|
||||||
|
```php
|
||||||
|
class LatestPostResolver implements OperationResolver
|
||||||
|
{
|
||||||
|
public function resolve($object, array $args, $context, ResolveInfo $info)
|
||||||
|
{
|
||||||
|
return Post::get()->sort('Date', 'DESC')->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**after**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\GraphQL\Schema\Registry\ResolverRegistry:
|
||||||
|
constructor:
|
||||||
|
myResolver: '%$MyProject\Resolvers\MyResolvers'
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
class MyResolvers extends DefaultResolverProvider
|
||||||
|
{
|
||||||
|
public static function resolveLatestPost($object, array $args, $context, ResolveInfo $info)
|
||||||
|
{
|
||||||
|
return Post::get()->sort('Date', 'DESC')->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This method relies on [resolver discovery](getting_started/working_with_generic_types/resolver_discovery),
|
||||||
|
which you can learn more about in the documentation.
|
||||||
|
|
||||||
|
Alternatively, you can hardcode the resolver into your config:
|
||||||
|
|
||||||
|
**app/_graphql/queries.yml**
|
||||||
|
```yaml
|
||||||
|
latestPost:
|
||||||
|
type: Post
|
||||||
|
resolver: ['MyResolvers', 'latestPost' ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## ScaffoldingProviders are now SchemaUpdaters
|
||||||
|
|
||||||
|
If you were updating your schema with procedural code, you'll need to change your `ScaffoldingProvider`
|
||||||
|
interface to `SchemaUpdater`, and use the `updateSchema(Schema $schema): void` function.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
Register your schema builder, and change the code.
|
||||||
|
|
||||||
|
**before**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Manager:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
scaffolding_providers:
|
||||||
|
- 'MyProject\MyProvider'
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
class MyProvider implements ScaffoldingProvider
|
||||||
|
{
|
||||||
|
public function provideGraphQLScaffolding(SchemaScaffolder $scaffolder)
|
||||||
|
{
|
||||||
|
// updates here...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**after**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Schema\Schema:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
builders:
|
||||||
|
- 'MyProject\MyProvider'
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
class MyProvider implements SchemaUpdater
|
||||||
|
{
|
||||||
|
public function updateSchema(Schema $schema): void
|
||||||
|
{
|
||||||
|
// updates here...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The API for procedural code has been **completely rewritten**. You'll need to rewrite all of the code
|
||||||
|
in these classes. For more information on working with procedural code, read the
|
||||||
|
[using procedural code](getting_started/using_procedual_code) documentation.
|
||||||
|
|
||||||
|
## Goodbye, scaffolding, hello models
|
||||||
|
|
||||||
|
In the 3.x release, a massive footprint of the codebase was dedicated to a DataObject-specific API called
|
||||||
|
"scaffolding" that was used to generate types, queries, fields, and more from the ORM. In 4.x, that
|
||||||
|
approach has been moved to concept called **model types**.
|
||||||
|
|
||||||
|
A model type is just a type that is backed by a class that express awareness of its schema (like a DataObject!).
|
||||||
|
At a high-level, it needs to answer questions like:
|
||||||
|
|
||||||
|
* Do you have field X?
|
||||||
|
What type is field Y?
|
||||||
|
* What are all the fields you offer?
|
||||||
|
* What operations do you provide?
|
||||||
|
* Do you require any extra types to be added to the schema?
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
The 4.x release ships with a model type implementation specifically for DataObjects, which you can use
|
||||||
|
a lot like the old scaffolding API.
|
||||||
|
|
||||||
|
**before**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\GraphQL\Manager:
|
||||||
|
schemas:
|
||||||
|
default:
|
||||||
|
scaffolding:
|
||||||
|
types:
|
||||||
|
SilverStripe\Security\Member:
|
||||||
|
fields: '*'
|
||||||
|
operations: '*'
|
||||||
|
SilverStripe\CMS\Model\SiteTree:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
content: true
|
||||||
|
operations:
|
||||||
|
read: true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**after**
|
||||||
|
|
||||||
|
**app/_graphql/models.yml**
|
||||||
|
```yaml
|
||||||
|
SilverStripe\Security\Member:
|
||||||
|
fields: '*'
|
||||||
|
operations: '*'
|
||||||
|
SilverStripe\CMS\Model\SiteTree:
|
||||||
|
fields:
|
||||||
|
title: true
|
||||||
|
content: true
|
||||||
|
operations:
|
||||||
|
read: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## DataObject field names are lowerCamelCase by default
|
||||||
|
|
||||||
|
The 3.x release of the module embraced an anti-pattern of using **UpperCamelCase** field names so that they could
|
||||||
|
map to the conventions of the ORM. This makes frontend code look awkward, and there's no great
|
||||||
|
reason for the Silverstripe CMS graphql server to break convention. In this major release,
|
||||||
|
the **lowerCamelCase** approach is encouraged.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
Change the casing in your queries.
|
||||||
|
|
||||||
|
**before**
|
||||||
|
```graphql
|
||||||
|
query readPages {
|
||||||
|
nodes {
|
||||||
|
Title
|
||||||
|
ShowInMenus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**after**
|
||||||
|
```graphql
|
||||||
|
query readPages {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
showInMenus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DataObject type names are simpler
|
||||||
|
|
||||||
|
To avoid naming collisions, the 3.x release of the module used a pretty aggressive approach to ensuring
|
||||||
|
uniqueness when converting a DataObject class name to a GraphQL type name, which was `<vendorName><shortName>`.
|
||||||
|
|
||||||
|
In the 4.x release, the typename is just the `shortName` by default, which is based on the assumption that
|
||||||
|
most of what you'll be exposing is in your own app code, so collisions aren't that likely.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
Change any references to DataObject type names in your queries
|
||||||
|
|
||||||
|
**before**
|
||||||
|
`query SilverStripeSiteTrees {}`
|
||||||
|
|
||||||
|
**after**
|
||||||
|
`query SiteTrees {}`
|
||||||
|
|
||||||
|
If this new pattern is not compatible with your set up (e.g. if you use feature-based namespacing), you have full
|
||||||
|
control over how types are named. You can use the `type_formatter` and `type_prefix` on `DataObjectModel` to
|
||||||
|
influence the naming computation. Read more about this in the [DataObject model type](getting_started/working_with_dataobjects/dataobject_model_type#customising-the-type-name) docs.
|
||||||
|
|
||||||
|
## The Connection class has been moved to plugins
|
||||||
|
|
||||||
|
In the 3.x release, you could wrap a query in the `Connection` class to add pagination features.
|
||||||
|
In 4.x, these features are provided via the new [plugin system](extending/plugins).
|
||||||
|
|
||||||
|
The good news is that all DataObject queries are paginated by default, and you shouldn't have to worry about
|
||||||
|
this, but if you are writing a custom query and want it paginated, check out the section on
|
||||||
|
[adding pagination to a custom query](getting_started/working_with_generic_types/adding_pagination).
|
||||||
|
|
||||||
|
Additionally, the sorting features that were provided by `Connection` have been moved to a plugin dedicated to
|
||||||
|
`SS_List` results. Again, this plugin is applied to all DataObjects by default, and will include all of their
|
||||||
|
sortable fields by default. This is configurable, however. See the
|
||||||
|
[query plugins](getting_started/working_with_dataobjects/query_plugins) section for more information.
|
||||||
|
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
There isn't much you have to do here to maintain compatibility. If you prefer to have a lot of control over
|
||||||
|
what your sort fields are, check out the linked documentation above.
|
||||||
|
|
||||||
|
## Query filtering has been moved to a plugin
|
||||||
|
|
||||||
|
The previous `QueryFilter` API has been vastly simplified in a new plugin. Filtering is provided to all
|
||||||
|
read queries by default, and should include all filterable fields, including nested relationships.
|
||||||
|
This is configurable, however. See the
|
||||||
|
[query plugins](getting_started/working_with_dataobjects/query_plugins) section for more information.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
There isn't much you have to do here to maintain compatibility. If you prefer to have a lot of control over
|
||||||
|
what your filter fields are, check out the linked documentation above.
|
||||||
|
|
||||||
|
## Query permissions have been moved to a plugin
|
||||||
|
|
||||||
|
This was mostly an internal API, and shouldn't be affected in an upgrade, but if you want more information
|
||||||
|
on how it works, you can [read the permissions documentation](getting_started/working_with_dataobjects/permissions).
|
||||||
|
|
||||||
|
## Enums are first-class citizens
|
||||||
|
|
||||||
|
In the 3.x release, there was no clear path to creating enum types, but in 4.x, they have a prime spot in the
|
||||||
|
configuration layer.
|
||||||
|
|
||||||
|
**before**
|
||||||
|
|
||||||
|
(A type creator that has been hacked to return an `Enum` singleton?)
|
||||||
|
|
||||||
|
**after**
|
||||||
|
|
||||||
|
**app/_graphql/enums.yml**
|
||||||
|
```yaml
|
||||||
|
Status:
|
||||||
|
SHIPPED: Shipped
|
||||||
|
CANCELLED: Cancelled
|
||||||
|
PENDING: Pending
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware signature is more loosely typed
|
||||||
|
|
||||||
|
In the 3.x release, `QueryMiddleware` was a very specific implementation that took parameters that were unique
|
||||||
|
to queries. The middleware pattern is now more generic and accepts a loosely-typed `params` array that can consist
|
||||||
|
of anything -- more like an `event` parameter for an event handler. If you've defined custom middleware, you'll
|
||||||
|
need to update it. Check out the [adding middleware](extending/adding_middleware) section for more information.
|
@ -1,20 +1,17 @@
|
|||||||
---
|
# Silverstripe CMS GraphQL server
|
||||||
title: GraphQL
|
|
||||||
summary: Learn how to create and customise GraphQL services on your Silverstripe CMS project.
|
|
||||||
introduction: Learn how to create and customise GraphQL services on your Silverstripe CMS project.
|
|
||||||
icon: cookie
|
|
||||||
---
|
|
||||||
|
|
||||||
[CHILDREN]
|
GraphQL is the content API layer for Silverstripe CMS. It is the
|
||||||
|
recommended way of getting data in and out of the content management
|
||||||
|
system.
|
||||||
|
|
||||||
## Additional documentation
|
For more information on GraphQL, visit its [documentation site](https://graphql.org).
|
||||||
|
|
||||||
A substantial part of the GraphQL documentation for Silverstripe CMS still
|
[CHILDREN includeFolders]
|
||||||
reside on the GraphQL module's _Readme_ file. We are in the process of folding
|
|
||||||
this information back into the main Silverstripe CMS documentation.
|
|
||||||
|
|
||||||
* [silverstripe/graphql](https://github.com/silverstripe/silverstripe-graphql/)
|
[alert]
|
||||||
|
You are viewing docs for a pre-release version of silverstripe/graphql (4.x).
|
||||||
## API Documentation
|
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).
|
||||||
* [GraphQL](api:SilverStripe\GraphQL)
|
Docs for the current stable version (3.x) can be found
|
||||||
|
[here](https://github.com/silverstripe/silverstripe-graphql/tree/3)
|
||||||
|
[/alert]
|
||||||
|
Loading…
Reference in New Issue
Block a user