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:
Aaron Carlino 2020-10-20 10:56:17 +13:00 committed by GitHub
parent 4670cd3af9
commit c1cda2b113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 4271 additions and 175 deletions

View File

@ -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)

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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 theyre 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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View 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]

View File

@ -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]

View File

@ -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]

View 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]

View File

@ -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]

View File

@ -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]

View File

@ -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'
```

View File

@ -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]

View 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]

View 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);
```

View 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.

View File

@ -1,20 +1,17 @@
---
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
---
# Silverstripe CMS GraphQL server
[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
reside on the GraphQL module's _Readme_ file. We are in the process of folding
this information back into the main Silverstripe CMS documentation.
[CHILDREN includeFolders]
* [silverstripe/graphql](https://github.com/silverstripe/silverstripe-graphql/)
## API Documentation
* [GraphQL](api:SilverStripe\GraphQL)
[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]